{"type":"title","v":1,"title":"","updatedAt":"2026-07-16T09:55:28.612Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f6a5a-44e7-7000-9f89-c6ee742a59b4","timestamp":"2026-07-16T09:55:28.612Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"aa803233","parentId":null,"timestamp":"2026-07-16T09:55:28.645Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"4150ac48","parentId":"aa803233","timestamp":"2026-07-16T09:55:28.646Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"5cbb8f8a","parentId":"4150ac48","timestamp":"2026-07-16T09:55:28.649Z","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: **Integration Test 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 `ReproHarness`. Currently visible peers:\n- `Main` — main (main, running)\n- `RefreshLifecycle` — Daemon Lifecycle Investigator (sub, running)\n- `PtyControl` — PTY Transport 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\nExisting integration/e2e tests and historical hazard documentation covering daemon refresh with attached PTYs. Non-goal: source-wide architecture summary.\n# Change\nLocate the closest real-process test harness that can start broker/brain, attach multiple controllers, perform daemon refresh or brain replacement, and assert bidirectional PTY traffic afterward. Identify how to extend or compose it into a deterministic feedback loop without touching live user sessions.\n# Acceptance\nReturn exact tests/helpers/commands, limitations, and the smallest deterministic pass/fail scenario matching the reported symptom. No edits or live refreshes.","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":"40e3ca80","parentId":"5cbb8f8a","timestamp":"2026-07-16T09:55:28.656Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\nExisting integration/e2e tests and historical hazard documentation covering daemon refresh with attached PTYs. Non-goal: source-wide architecture summary.\n# Change\nLocate the closest real-process test harness that can start broker/brain, attach multiple controllers, perform daemon refresh or brain replacement, and assert bidirectional PTY traffic afterward. Identify how to extend or compose it into a deterministic feedback loop without touching live user sessions.\n# Acceptance\nReturn exact tests/helpers/commands, limitations, and the smallest deterministic pass/fail scenario matching the reported symptom. No edits or live refreshes."}],"attribution":"agent","timestamp":1784195728651}}
{"type":"message","id":"ddadf86e","parentId":"40e3ca80","timestamp":"2026-07-16T09:55:38.197Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning investigation of diagnose skill and globbing tests**"},{"type":"toolCall","id":"call_JwMLdEnjS0N3nSsZVmy5tyul|fc_0b42584a8ab0545d016a58aa9a26a081998f56150f92633734","name":"read","arguments":{"i":"Reading diagnosis workflow","path":"skill://diagnose","selector":null}},{"type":"toolCall","id":"call_WfBXptmC1W3Vgzubwu0UM9ff|fc_0b42584a8ab0545d016a58aa9a26ac8199b080d9b648332520","name":"glob","arguments":{"i":"Mapping required documents","path":"CONTEXT.md;docs/KNOWN-HAZARDS.md;docs/DEBUG-ROLLOUT.md;docs/adr/**/*;docs/ADR*;**/*refresh*;**/*integration*;**/*e2e*","hidden":true,"gitignore":true,"limit":300}},{"type":"toolCall","id":"call_ZZFGCRj2ybXlPTDl9he02bKs|fc_0b42584a8ab0545d016a58aa9a26b481999d8e5e53c5776a4b","name":"grep","arguments":{"i":"Finding refresh tests","pattern":"daemon refresh|brain replacement|replace.*brain|refresh.*daemon|DaemonRefresh|refreshDaemon","path":"tests;test;src;docs;crates;packages","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_mGAyfvRYctaZLmdGHEqkvlyl|fc_0b42584a8ab0545d016a58aa9a26bc8199a561966aa369e4ff","name":"grep","arguments":{"i":"Finding PTY harnesses","pattern":"broker|brain|controller|attach|pty|bidirectional|real.process|child_process|spawn","path":"tests;test;src;docs;crates;packages","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14532,"output":347,"cacheRead":0,"cacheWrite":0,"totalTokens":14879,"reasoningTokens":77,"cost":{"input":0.07266,"output":0.01041,"cacheRead":0,"cacheWrite":0,"total":0.08307}},"stopReason":"toolUse","timestamp":1784195728712,"responseId":"resp_0b42584a8ab0545d016a58aa917214819983de207e5eeb6b9f","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aa92e3d88199bc257f409a5423b2","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqaFRN0quC6UhAJZpFcpSmZX4zllEk2fmbN70INKVX1LMcV-ZXuyT1_xr2slUZqRp6vaRQ4STCwaYhGMptj01Mbc7mmL4250zUhAMX_H7ksL_ndwSKBQeuN7vAVWQASTwjKZcVQeztfYSBl9CqGP69-qJKxZfEgjiir3iZFoXFDvIv4wrt-c5YK02GMsdwzyznxK2xJnthCOvSjqWagoMAGwh4dp09CoZlYSRl7RENfxUCHc6566ilz6OAPjWjNkDSMLdf50gnSq31ubIGgNWMv6uyqX7n8jP6VhRWxZmDYj-QhAE7UDbmB6kafm2N2DtOoVNdojVsKrv_T2HYuC1ed1_MK3WZIf9c-3OnN9uBnY9t7GJpJUlTpvpMDS4qlO3SMDZT4FBKz0pgi3rFGyA_nZR5wh6W0UMl_zV-FSbj7eHfY-Tw-Xy7IlCMobaZIOdKubttz0lc7E0a_mU3SrUa34_Ulvv-uhWCT3gqZ7qtB1xTBJGOsYQ__kpFd0itwwmqVSCaTYyDujP_86S1yO2gzcT3dm-CT7bPQmbCnDqvwMsY-0WDciejwU4SoxFLGQ8G7vp1i5epTxOyMtI9e5_0KxarsPxxRD6A9rhm_por_7fMELIFss1WaH0Dh_6PfpZQM1cOtiNIGzT5QIz10zIZksMT9otQaUSXJclysTh1qA688VUbISGAJauvMvI36jrqGcBUCQ0erShGUi6betHsaDRV85bt9Ioh6hPCkfHemIF-IQJtE6Ao4PYChtbDlY5WGew8UgDQvKR4j7X6_2t6WizjS2jSZgqdYTmkO_VPgZxW9TdP6u6boa6vC0OIsAd7kgdSEBU4iCdMvM-deBKRLlLN-jZXIYO4CXlWKq66YIe6xpS7L8b7e5PyU59iZfD1vk-BRLsrXrSKWhiymRTWe1Qpf2Ue0CQkvFJMD4wr7wyVZqmEH0UHBgc46JAZLv1vL5NN2M-GXL23Vqbj727WP6Yg9nsX0eLH3_x_85Fs9AgB-SfWTFFVmheDRS2nGxATR2N2rIHja1gENqnL6w2XuesAAntaz3ByUufLWIjYb_jbRRf7z1HNbvBDqtum4MW5d4DLUMg5lwHF0n4V-7XqMyYoh7LUTcnAzEQE4bL71dE1WQ342WQwdPqnJLkoJYYHmbwF6OgBAyaLzuWvjQK14Trqa1aXS_WARexw8Bn-96Co8pkBoFbaTcIgLo7IkWru_ymaqJsD6_ZEGTKngxCCMkB8j7p5ShuzanxdnA1gkdI9tSbbSAvzVeJ4G9acwCIZ5eaJFSs7_ZQBRB1xx7FyrmkuM255pa0CjVZGHNh-nOUNWZHnnx8YnOWnxtH2BPNG0cH7zOp_5eCgoxoIQ3egiL4JI-J7lp_ZHSIlIrNgfgWXrdjtalbS6X4QYikhD7avJT-y1Zvyc-qRdu1qjWD-byQ==","summary":[{"type":"summary_text","text":"**Planning investigation of diagnose skill and globbing tests**"}]},{"id":"fc_0b42584a8ab0545d016a58aa9a26a081998f56150f92633734","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading diagnosis workflow\",\"path\":\"skill://diagnose\",\"selector\":null}","call_id":"call_JwMLdEnjS0N3nSsZVmy5tyul","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aa9a26ac8199b080d9b648332520","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping required documents\",\"path\":\"CONTEXT.md;docs/KNOWN-HAZARDS.md;docs/DEBUG-ROLLOUT.md;docs/adr/**/*;docs/ADR*;**/*refresh*;**/*integration*;**/*e2e*\",\"hidden\":true,\"gitignore\":true,\"limit\":300}","call_id":"call_WfBXptmC1W3Vgzubwu0UM9ff","name":"glob"},{"id":"fc_0b42584a8ab0545d016a58aa9a26b481999d8e5e53c5776a4b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding refresh tests\",\"pattern\":\"daemon refresh|brain replacement|replace.*brain|refresh.*daemon|DaemonRefresh|refreshDaemon\",\"path\":\"tests;test;src;docs;crates;packages\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_ZZFGCRj2ybXlPTDl9he02bKs","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aa9a26bc8199a561966aa369e4ff","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding PTY harnesses\",\"pattern\":\"broker|brain|controller|attach|pty|bidirectional|real.process|child_process|spawn\",\"path\":\"tests;test;src;docs;crates;packages\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_mGAyfvRYctaZLmdGHEqkvlyl","name":"grep"}]},"duration":9484.05459999945,"ttft":2159.674599999562,"contextSnapshot":{"promptTokens":14532,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JwMLdEnjS0N3nSsZVmy5tyul|fc_0b42584a8ab0545d016a58aa9a26a081998f56150f92633734","toolName":"read","startedAt":"2026-07-16T09:55:38.197Z","args":{"path":"skill://diagnose"},"intent":"Reading diagnosis workflow"},"id":"fbb5e883","parentId":"ddadf86e","timestamp":"2026-07-16T09:55:38.197Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WfBXptmC1W3Vgzubwu0UM9ff|fc_0b42584a8ab0545d016a58aa9a26ac8199b080d9b648332520","toolName":"glob","startedAt":"2026-07-16T09:55:38.197Z","args":{"path":"CONTEXT.md;docs/KNOWN-HAZARDS.md;docs/DEBUG-ROLLOUT.md;docs/adr/**/*;docs/ADR*;**/*refresh*;**/*integration*;**/*e2e*"},"intent":"Mapping required documents"},"id":"823bde10","parentId":"fbb5e883","timestamp":"2026-07-16T09:55:38.197Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZZFGCRj2ybXlPTDl9he02bKs|fc_0b42584a8ab0545d016a58aa9a26b481999d8e5e53c5776a4b","toolName":"grep","startedAt":"2026-07-16T09:55:38.197Z","args":{"path":"tests;test;src;docs;crates;packages"},"intent":"Finding refresh tests"},"id":"cc02adb2","parentId":"823bde10","timestamp":"2026-07-16T09:55:38.197Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mGAyfvRYctaZLmdGHEqkvlyl|fc_0b42584a8ab0545d016a58aa9a26bc8199a561966aa369e4ff","toolName":"grep","startedAt":"2026-07-16T09:55:38.197Z","args":{"path":"tests;test;src;docs;crates;packages"},"intent":"Finding PTY harnesses"},"id":"bd334505","parentId":"cc02adb2","timestamp":"2026-07-16T09:55:38.197Z"}
{"type":"message","id":"0e1cd200","parentId":"bd334505","timestamp":"2026-07-16T09:55:38.200Z","message":{"role":"toolResult","toolCallId":"call_JwMLdEnjS0N3nSsZVmy5tyul|fc_0b42584a8ab0545d016a58aa9a26a081998f56150f92633734","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":1784195738199}}
{"type":"message","id":"d8673c83","parentId":"0e1cd200","timestamp":"2026-07-16T09:55:38.212Z","message":{"role":"toolResult","toolCallId":"call_WfBXptmC1W3Vgzubwu0UM9ff|fc_0b42584a8ab0545d016a58aa9a26ac8199b080d9b648332520","toolName":"glob","content":[{"type":"text","text":"CONTEXT.md\n# docs-site/src/harness-contract/\nintegration-checklist.md\n# crates/\n## spt/tests/\nprojindex_reader_e2e.rs\ndummy_harness_e2e.rs\nprojindex_writer_e2e.rs\nwhoami_identity_e2e.rs\ndaemon_refresh_e2e.rs\ncomposite_e2e.rs\ndocs_bundle_e2e.rs\nrelease_verify_e2e.rs\nbootstrap_verb_e2e.rs\ntrial_drain_drive_e2e.rs\nresume_no_control_steal_e2e.rs\nsend_stamp_agent_id_e2e.rs\nendpoint_autostart_e2e.rs\nlist_json_liveness_parity_e2e.rs\nlisten_seed_retry_e2e.rs\nactive_only_never_relay_e2e.rs\nattach_wedge_e2e.rs\nworker_visibility_e2e.rs\nworker_lifecycle_e2e.rs\npsyche_download_e2e.rs\nnotify_shell_e2e.rs\nmulti_subnet_bringup_e2e.rs\nlive_adapt_translation_swap_e2e.rs\ntunnel_e2e.rs\nshell_sleepwake_e2e.rs\nshell_e2e.rs\nshell_actgate_e2e.rs\nrun_no_dup_session_e2e.rs\nresume_template_e2e.rs\nready_resume_ledger_e2e.rs\nquickstart_e2e.rs\npsyche_sid_custody_e2e.rs\npoll_envelope_e2e.rs\noneliner_e2e.rs\nnested_resolution_e2e.rs\nlive_resolve_e2e.rs\nlive_firsthost_e2e.rs\nlive_bind_firsthost_e2e.rs\nlivehost_bootgate_e2e.rs\njob_escape_e2e.rs\nidle_edge_drain_e2e.rs\ngateway_owner_shell_e2e.rs\ngateway_e2e.rs\ndrive_e2e.rs\ncontract_e2e.rs\nboundary_ready_strand_e2e.rs\nbind_honest_cross_perch_e2e.rs\nbind_cwd_project_e2e.rs\nbind_adapter_profile_persist_e2e.rs\n## spt-daemon/tests/\ndocs_server_e2e.rs\ndaemon_e2e.rs\npsyche_residency_expectation_e2e.rs\npsyche_event_turn_e2e.rs\npsyche_context_file_e2e.rs\nlegacy_resident_sweep_e2e.rs\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":"CONTEXT.md, docs/KNOWN-HAZARDS.md, docs/DEBUG-ROLLOUT.md, docs/adr/**/*, docs/ADR*, **/*refresh*, **/*integration*, **/*e2e*","fileCount":95,"files":["docs-site/src/harness-contract/integration-checklist.md","CONTEXT.md","crates/spt/tests/projindex_reader_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","docs/adr/0037-materialized-project-index.md","crates/spt/tests/projindex_writer_e2e.rs","crates/spt/tests/whoami_identity_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/composite_e2e.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt/tests/release_verify_e2e.rs","crates/spt/tests/bootstrap_verb_e2e.rs","docs/adr/0036-private-fork-distribution.md","docs/KNOWN-HAZARDS.md","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/send_stamp_agent_id_e2e.rs","crates/spt/tests/endpoint_autostart_e2e.rs","crates/spt/tests/list_json_liveness_parity_e2e.rs","crates/spt/tests/listen_seed_retry_e2e.rs","crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/tests/attach_wedge_e2e.rs","docs/adr/0005-totp-seeded-spake2-node-pairing.md","crates/spt/tests/worker_visibility_e2e.rs","crates/spt/tests/worker_lifecycle_e2e.rs","docs/adr/0031-server-side-screen-grid-render-repaint.md","docs/adr/0034-per-minter-op-id-namespacing.md","crates/spt/tests/psyche_download_e2e.rs","crates/spt/tests/notify_shell_e2e.rs","crates/spt/tests/multi_subnet_bringup_e2e.rs","crates/spt/tests/live_adapt_translation_swap_e2e.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","crates/spt-daemon/tests/psyche_event_turn_e2e.rs","crates/spt-daemon/tests/psyche_context_file_e2e.rs","crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","crates/spt/tests/tunnel_e2e.rs","crates/spt/tests/shell_sleepwake_e2e.rs","crates/spt/tests/shell_e2e.rs","crates/spt/tests/shell_actgate_e2e.rs","crates/spt/tests/run_no_dup_session_e2e.rs","crates/spt/tests/resume_template_e2e.rs","crates/spt/tests/ready_resume_ledger_e2e.rs","crates/spt/tests/quickstart_e2e.rs","crates/spt/tests/psyche_sid_custody_e2e.rs","crates/spt/tests/poll_envelope_e2e.rs","crates/spt/tests/oneliner_e2e.rs","crates/spt/tests/nested_resolution_e2e.rs","crates/spt/tests/live_resolve_e2e.rs","crates/spt/tests/live_firsthost_e2e.rs","crates/spt/tests/live_bind_firsthost_e2e.rs","crates/spt/tests/livehost_bootgate_e2e.rs","crates/spt/tests/job_escape_e2e.rs","crates/spt/tests/idle_edge_drain_e2e.rs","crates/spt/tests/gateway_owner_shell_e2e.rs","crates/spt/tests/gateway_e2e.rs","crates/spt/tests/drive_e2e.rs","crates/spt/tests/contract_e2e.rs","crates/spt/tests/boundary_ready_strand_e2e.rs","crates/spt/tests/bind_honest_cross_perch_e2e.rs","crates/spt/tests/bind_cwd_project_e2e.rs","crates/spt/tests/bind_adapter_profile_persist_e2e.rs","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":1784195738212}}
{"type":"message","id":"90473f77","parentId":"d8673c83","timestamp":"2026-07-16T09:55:38.270Z","message":{"role":"toolResult","toolCallId":"call_ZZFGCRj2ybXlPTDl9he02bKs|fc_0b42584a8ab0545d016a58aa9a26b481999d8e5e53c5776a4b","toolName":"grep","content":[{"type":"text","text":"# docs/\n## KNOWN-HAZARDS.md\n 620|- **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 e...\n*621|- **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 ...\n 622|- **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 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## MANIFEST.md\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## STORAGE.md\n 189|  file each; the daemon's writer drains, debounces, and coalesces them into\n*190|  one refresh. Fire-and-forget and daemon-offline-safe (events accumulate\n 191|  and are consumed at the next brain start).\n 192|  <!-- [doc->REQ-PROJECT-INDEX-INVALIDATION] -->\n 193|\n## THE-FORKENING-TRIAGE.md\n 93|  Retires findings-backlog seed #12 (REQ-UPDATE-ONE-STEP-SAFE-SWAP class).\n*94|- `spt daemon refresh` — restart the brain WITHOUT a binary swap and WITHOUT\n 95|  touching the broker (the apply_staged brain-cycle path minus the swap).\n 96|  Recovery verb for wedged brain-held state (e.g. broken endpoint bringup)\n 97|  that today needs a full daemon bounce.\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 22|\n*23|Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.\n 24|\n 25|Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).\n 26|\n### 0025-live-daemon-coordinated-adapter-update.md\n 37|\n*38|- **Refuse-while-running (ADR-0016 broker parity)** — update refuses if any endpoint on the adapter is live, lists them, and tells the operator to stop them first. Simple and consistent with the core broker-replace stance, but it is exactly the UX spt's on-the-fly objective rejects, and it does not deliver the live-update the brain already has. Rejected for adapters.\n 39|- **CLI-only swap with a retry/backoff on the lock** — does nothing for the stale in-memory manifest, and on Windows a running binary's lock never clears without stopping the process. Rejected.\n*40|- **Daemon-coordinated stop → CRC-swap → manifest-refresh → restart (chosen)** — the adapter analog of brain self-update; the only option that fixes both the locked-binary failure and the stale-manifest, at the cost of a daemon IPC apply path plus the per-endpoint resident-children registry.\n 41|\n 42|## Amendment (W3 build, 2026-06-22)\n 43|\n### 0034-per-minter-op-id-namespacing.md\n 89|- **Every op carries its minter as a TYPE invariant**: a `MintedOp{minter,\n*90|  seq}` newtype replaces bare `op_id: u64` through the brain/daemon threading\n 91|  paths, so an unstamped op is uncompilable — the minter resolves at the\n 92|  genuine minting site and threads by construction, not by discipline. The\n 93|  wire keeps the additive optional field (legacy materialized at broker\n\n## spikes/\n### SPIKE-01-broker-handoff.md\n 28|\n*29|The ownership model works: brain death is just a closed forwarding socket; the broker's held handles keep the child and the live client stream intact, and the broker-owned writer still delivers input after the brain is replaced. This is the *brain-only* update class — the one ADR-0004 must preserve, and the routine case.\n 30|\n 31|## Key finding — ConPTY DSR stall (not in any doc before)\n 32|\n\n# crates/\n\n## spt-daemon/src/\n### applyhost.rs\n 330|    // no manual bounce, every broker-held endpoint untouched at the process\n*331|    // level. This replaces the old in-process `apply_brain_only` handoff (the\n 332|    // regression: it re-attached a CLI-side subscriber while the live brain kept\n 333|    // running the OLD code). `classify` already gated this as brain-only, so the\n 334|    // broker never moves; only the brain restarts.\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### brain.rs\n 1007|    /// from the broker's per-session delivered cursor (`resume_seq`). This is the\n*1008|    /// production replacement for the retired single-session `BrainState` handoff\n 1009|    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs\n 1010|    /// all session continuity by querying the persistent side, never a brain→brain\n 1011|    /// message. Returns the ids re-attached (empty when the broker hosts none —\n\n## spt-store/src/\n### info.rs\n 85|    pub status: Option<String>,\n*86|    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this\n 87|    /// endpoint's activity (pulse tick / input / bind). The recency signal the\n 88|    /// consent gate resolves \"most-recently-active live session\" against\n 89|    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never\n\n## spt/\n\n### src/\n#### cli.rs\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...\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### 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...\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...\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\nSkipped missing paths: test, packages\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"tests, src, docs, crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":23,"fileCount":13,"files":["docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/STORAGE.md","docs/THE-FORKENING-TRIAGE.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md","docs/adr/0034-per-minter-op-id-namespacing.md","docs/spikes/SPIKE-01-broker-handoff.md","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brain.rs","crates/spt-store/src/info.rs","crates/spt/src/cli.rs","crates/spt/tests/daemon_refresh_e2e.rs"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":1},{"path":"docs/MANIFEST.md","count":1},{"path":"docs/STORAGE.md","count":1},{"path":"docs/THE-FORKENING-TRIAGE.md","count":1},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":1},{"path":"docs/adr/0025-live-daemon-coordinated-adapter-update.md","count":2},{"path":"docs/adr/0034-per-minter-op-id-namespacing.md","count":1},{"path":"docs/spikes/SPIKE-01-broker-handoff.md","count":1},{"path":"crates/spt-daemon/src/applyhost.rs","count":2},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-store/src/info.rs","count":1},{"path":"crates/spt/src/cli.rs","count":6},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":4}],"truncated":true,"displayContent":"# docs/\n## KNOWN-HAZARDS.md\n 620│- **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 e...\n*621│- **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 ...\n 622│- **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 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## MANIFEST.md\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## STORAGE.md\n 189│  file each; the daemon's writer drains, debounces, and coalesces them into\n*190│  one refresh. Fire-and-forget and daemon-offline-safe (events accumulate\n 191│  and are consumed at the next brain start).\n 192│  <!-- [doc->REQ-PROJECT-INDEX-INVALIDATION] -->\n 193│\n## THE-FORKENING-TRIAGE.md\n 93│  Retires findings-backlog seed #12 (REQ-UPDATE-ONE-STEP-SAFE-SWAP class).\n*94│- `spt daemon refresh` — restart the brain WITHOUT a binary swap and WITHOUT\n 95│  touching the broker (the apply_staged brain-cycle path minus the swap).\n 96│  Recovery verb for wedged brain-held state (e.g. broken endpoint bringup)\n 97│  that today needs a full daemon bounce.\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 22│\n*23│Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.\n 24│\n 25│Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).\n 26│\n### 0025-live-daemon-coordinated-adapter-update.md\n 37│\n*38│- **Refuse-while-running (ADR-0016 broker parity)** — update refuses if any endpoint on the adapter is live, lists them, and tells the operator to stop them first. Simple and consistent with the core broker-replace stance, but it is exactly the UX spt's on-the-fly objective rejects, and it does not deliver the live-update the brain already has. Rejected for adapters.\n 39│- **CLI-only swap with a retry/backoff on the lock** — does nothing for the stale in-memory manifest, and on Windows a running binary's lock never clears without stopping the process. Rejected.\n*40│- **Daemon-coordinated stop → CRC-swap → manifest-refresh → restart (chosen)** — the adapter analog of brain self-update; the only option that fixes both the locked-binary failure and the stale-manifest, at the cost of a daemon IPC apply path plus the per-endpoint resident-children registry.\n 41│\n 42│## Amendment (W3 build, 2026-06-22)\n 43│\n### 0034-per-minter-op-id-namespacing.md\n 89│- **Every op carries its minter as a TYPE invariant**: a `MintedOp{minter,\n*90│  seq}` newtype replaces bare `op_id: u64` through the brain/daemon threading\n 91│  paths, so an unstamped op is uncompilable — the minter resolves at the\n 92│  genuine minting site and threads by construction, not by discipline. The\n 93│  wire keeps the additive optional field (legacy materialized at broker\n\n## spikes/\n### SPIKE-01-broker-handoff.md\n 28│\n*29│The ownership model works: brain death is just a closed forwarding socket; the broker's held handles keep the child and the live client stream intact, and the broker-owned writer still delivers input after the brain is replaced. This is the *brain-only* update class — the one ADR-0004 must preserve, and the routine case.\n 30│\n 31│## Key finding — ConPTY DSR stall (not in any doc before)\n 32│\n\n# crates/\n\n## spt-daemon/src/\n### applyhost.rs\n 330│    // no manual bounce, every broker-held endpoint untouched at the process\n*331│    // level. This replaces the old in-process `apply_brain_only` handoff (the\n 332│    // regression: it re-attached a CLI-side subscriber while the live brain kept\n 333│    // running the OLD code). `classify` already gated this as brain-only, so the\n 334│    // broker never moves; only the brain restarts.\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### brain.rs\n 1007│    /// from the broker's per-session delivered cursor (`resume_seq`). This is the\n*1008│    /// production replacement for the retired single-session `BrainState` handoff\n 1009│    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs\n 1010│    /// all session continuity by querying the persistent side, never a brain→brain\n 1011│    /// message. Returns the ids re-attached (empty when the broker hosts none —\n\n## spt-store/src/\n### info.rs\n 85│    pub status: Option<String>,\n*86│    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this\n 87│    /// endpoint's activity (pulse tick / input / bind). The recency signal the\n 88│    /// consent gate resolves \"most-recently-active live session\" against\n 89│    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never\n\n## spt/\n\n### src/\n#### cli.rs\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     │...\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### 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    │...\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    │...\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│    );","missingPaths":["test","packages"],"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195738270}}
{"type":"message","id":"1f77e354","parentId":"90473f77","timestamp":"2026-07-16T09:55:38.329Z","message":{"role":"toolResult","toolCallId":"call_mGAyfvRYctaZLmdGHEqkvlyl|fc_0b42584a8ab0545d016a58aa9a26bc8199a561966aa369e4ff","toolName":"grep","content":[{"type":"text","text":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n*1|# Broker/Brain Split Restoration — Design Rationale (pre-ADR)\n 2|\n*3|**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` — see §0); six amendments folded in (marked **[V1]**–**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 — restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...\n 4|\n 5|**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.\n 6|\n...\n 12|\n*13|**Verdict: the core holds.** Confirmed at source — broker is an in-process thread (no broker child-spawn exists anywhere in the repo); no doc/plan/commit marks the collapse deliberate (silent drift; regression framing correct); `update.rs:233-234`'s \"exec the new binary's brain\" is aspirational and never wired; `applyhost.rs:176` records `applied` before the handoff (the optimistic `applied.json` watched on `enlyzeam`); `applyhost.rs:238-239` re-attaches `sessions.first()` with `from_seq=0` (both Q6 ga...\n 14|\n*15|**Two audit corrections folded in:** the NetHost is **already broker-owned** (`broker.rs:175` `OnceLock`, per the §B M4-D4a note), so the Q5 net item is near-free, not a migration; the supervise-backoff is wired at `peerloop.rs:805` (M8-D4), confirming Q7's reuse claim.\n 16|\n 17|**Six amendments raised and accepted** — each folded into the relevant section below and marked **[V1]**–**[V6]**.\n 18|\n...\n 34|\n*35|1. The hot-swap that *is* wired into `apply` is a **per-session brain handoff** (`apply_brain_only` → `Brain::handoff`), whose job is endpoint survival (REQ-UPD-3), not running-process replacement.\n*36|2. By explicit design, `applyhost.rs:36-38` states: *\"The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary); that restart is routine lifecycle, never an endpoint cycle.\"*\n 37|3. So new code only goes live on a *subsequent, unrelated* daemon restart (next logon, or a manual restart — which is what made the *other* node, `hfenduleam`, show a new pid). The practical consequence on `enlyzeam`: `gerald`'s live daemon kept running 0.3.0 — including the message-envelope codec — so the box from the original bug report **was still reproducing the original `\\r` corruption even though the fix sat on disk.**\n 38|\n 39|The operator then asked the real question this document answers: **what would it take to make an update actually hand off to the new executable seamlessly — no dropped processes, internal loop timings consistent across the swap, plus any other angles that matter?**\n...\n 48|\n*49|ADR-0004 (*\"Single consolidated daemon with broker/brain split; peer-propagated gated self-update\"*, **accepted 2026-05-29**) decided a **two-layer split implemented as two processes**:\n 50|\n*51|- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n*52|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53|\n 54|Explicit evidence the split was meant to be a **process** boundary:\n*55|- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n*56|- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n*57|- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n*58|- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n 59|\n 60|### 2.2 What production actually does\n 61|\n*62|The production daemon runs the broker as a **background thread inside the single `spt daemon` process**:\n 63|\n*64|- `daemon.rs:41-44` — *\"bind the broker (served on a background thread) + the seed-control channel (the foreground loop).\"*\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 70|\n...\n 72|\n*73|- 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. So **the no-endpoint-drop self-update pillar is silently unrealized.**\n*74|- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that \"the live daemon execs the verified new binary\" is **aspirational; it was never wired** because the process split it depends on does not exist in production.\n 75|- Updates do not run until an unrelated restart/logon (observed live, Section 1).\n 76|\n 77|This document's decisions restore ADR-0004's intended model.\n 78|\n*79|> **Glossary note already applied:** `CONTEXT.md` was sharpened during the session to state explicitly that there is **one broker per machine** (per `SPT_HOME`), *not* one per endpoint — a single broker hosts every endpoint's resources and is present even with zero endpoints online. This corrected a latent \"one broker per endpoint\" misconception and is load-bearing for decision Q2.\n 80|\n 81|---\n 82|\n## CONTEXT-MEMORY.md\n 36|\n*37|A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-cl...\n 38|\n 39|### Ownership\n 40|\n## DEBUG-CONVERGE-PLAN.md\n 69|| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |\n*70|| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |\n 71|| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |\n 72|\n 73|Convergence = every expected node is `Applied`. Any node still pending when the\n## DEBUG-ROLLOUT.md\n 24|  known-good binary.\n*25|- A broker-touching candidate may be staged, but apply refuses it while\n*26|  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n 27|  first flow does not auto-cycle them.\n 28|\n 29|## One-time lab setup\n...\n 107|  current debug-channel version.\n*108|- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n 109|  resources; quiesce hosted endpoints first.\n 110|\n 111|## Agent checklist\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 15|| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n 16|| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n*17|| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n 18|| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19|| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20|| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n*21|| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n 22|| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-u...\n 23|| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner...\n*24|| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8...\n 25|| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruni...\n 26|| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27|| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n 28|| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\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...\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*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## DOCS-STRATEGY.md\n 123|`Messaging → Lifecycle → Terminal → Networking → Harness contract → Instances → Shells → Self-update`\n*124|(messaging is the primitive the quickstart needs; self-update last because its no-termination invariant depends on the terminal/broker model.)\n 125|**Per-vertical internal template (Django labels × Cloudflare ordering):**\n 126|`Overview (why+diagram) · Quickstart/Tutorial · How-to guides · Reference (rustdoc+CLI+schema) · llms.txt`\n 127|\n## DORMANCY-BUDGET.md\n 15|\n*16|1. binds a real (net-less) broker — the production PTY-hosting path — in a\n 17|   hermetic `SPT_HOME`;\n*18|2. spawns N idle seats through `Brain::spawn_session_pid` (one probe brain per\n 19|   seat, each labeled `budget-<i>`), exactly the shape of a **dormant** seat:\n*20|   PTY master + harness child + output ring, state-preserved and undriven;\n 21|3. settles, then samples each child's **RSS**, **handle/fd count**, and\n 22|   **cumulative CPU** twice across an idle window — the CPU delta is the idle\n 23|   burn of a warm seat;\n*24|4. samples the host process's RSS before/after spawning (the broker-side\n*25|   per-seat overhead: PTY plumbing + output rings);\n 26|5. kills every seat (the **suspended** state — only the on-disk perch record\n 27|   would remain in production) and samples the residual.\n 28|\n...\n 31|count, and `/proc/<pid>/stat` utime+stime. **Limitation:** the sample covers\n*32|the direct PTY child only — a harness that forks a worker tree under-reports\n 33|by the tree's cost (the claude numbers below are the main node process, which\n 34|dominates).\n 35|\n...\n 61|\n*62|Broker-side overhead: ~0.2–0.3 MiB per seat (RSS delta ÷ N). Suspended\n 63|residual: host process returns to its ~6–7 MiB baseline — a suspended seat\n 64|costs **only the on-disk perch record** (bytes, not megabytes, no handles, no\n 65|CPU).\n...\n 78|\n*79|Broker-side overhead: ~0.3 MiB per seat. Suspended residual: host process\n 80|back at its ~4.7 MiB baseline.\n 81|\n 82|## The policy (locked by these numbers)\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n*1|# F-027 — ENDPOINT_SPAWN_FAIL: pre-spawn seam + bind-deadline honesty (DESIGN)\n 2|\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 8|\n*9|Scope split: **Half A (bind-deadline honesty) and Half B (pre-spawn manifest\n 10|seam) are spt-core changes** (todlando's wave, sequenced after F-029). **The\n 11|trust-seeder binary itself is adapter-side** (perri's wave — spt-core stays\n 12|harness-agnostic per the glue-model boundary: adapters = manifest + binary only;\n...\n 18|\n*19|`spt endpoint run <id> --start` prints `ENDPOINT_RUN_STARTED` when the broker\n*20|has accepted the spawn RPC and a child PID exists — **nothing more is verified**\n 21|(cli.rs:1819 machine line, cli.rs:1833 `--start` success print). A harness that\n*22|launches but wedges post-spawn (workspace-trust prompt, null stdin) never fires\n 23|its SessionStart hook, so `establish_perch` never runs (api/startup.rs:299 is\n 24|the ONLY writer of the `ready` marker), and the endpoint sits `ready=false`\n 25|**forever, silently** — while the operator was told it started.\n...\n 28|cli.rs:2735/3826) but nothing gates or alarms on it: passive display only. The\n*29|attach path's `await_endpoint_session` (cli.rs:1854, 25 s) gates on broker\n*30|**session existence** — created synchronously at spawn, so it passes instantly\n 31|for a wedged harness — and the `--start` path skips even that.\n 32|\n*33|Precedent trap to avoid: `TRANSLATION_SPAWN_FAILED` (broker.rs:1271) is loud on\n 34|the **detached daemon's stderr, which nobody reads** (ADR-0022:130,\n 35|KNOWN-HAZARDS F-019 lesson). Any F-027 token must land on **CLI stderr and/or\n 36|the perch record**, never daemon-stderr-only.\n...\n 42|| 1 | TRUST-WEDGE — CC at workspace-trust dialog, null stdin; `ENDPOINT_RUN_STARTED` printed; ready=false forever | every fresh probe cwd, this session | SILENT |\n*43|| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n*44|| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n*45|| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n*46|| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n*47|| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n*48|| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n 49|\n 50|Seed-1 correction (accepted): the \"backslash-keyed `hasTrustDialogAccepted`\n 51|silently ignored\" pin from the RUNTRUTH triage was a **rig artifact** (hand-\n...\n 57|Seed-2 (the deeper root): trust is stored **per config root**, and the root a\n*58|spawn resolves is a **function of whether `CLAUDE_CONFIG_DIR` survives the\n*59|spawn env-rebuild** — which differs by spawn mode (detached daemon\n*60|`CreateEnvironmentBlock` rebuild vs foreground inherit). Two spawns on one box\n 61|resolved different accounts (Reavo vs Brandon). Seeding the operator shell's\n 62|root is therefore WRONG in general.\n 63|\n...\n 67|The silent class (modes 1, 2, 3, 6) must fail with the same loudness as the\n*68|pre-spawn class (modes 4, 5) already does.\n 69|\n 70|### Half A — bind-deadline honesty (spt-core, harness-agnostic)\n 71|\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 20|\n*21|- `livehost.rs:497 → host_one:518` spawns the psyche\n*22|  (`spawn_psyche_owned(Some(&session_id), None)`) and starts `run_pulse_loop`\n 23|  as its driver; `confirm_residency_or_unhost`\n 24|  (REQ-HAZARD-LIVEHOST-NONRESIDENT) probes the owned child and re-hosts every\n 25|  ~5 s reconcile tick when it isn't resident.\n...\n 27|  production callers**: `resume_psyche` (spt-live resume.rs:175, mode-validated\n*28|  spawn + nested rebind), `run_psyche_turn` (spt-live turn.rs:72, bounded\n 29|  stdin→stdout turn; its only production caller is the ADR-0013 conflict-merge\n 30|  in reconcile.rs), and `psyche_turn_and_relay` (spt-daemon psyrelay.rs:168,\n 31|  turn + intent-parse + sanitized relay).\n...\n 70|| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |\n*71|| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |\n 72|| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |\n 73|| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |\n 74|| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |\n...\n 80|\n*81|### W1 — wire the per-event driver (retire the resident spawn)\n 82|\n*83|`host_one` stops spawning a resident psyche. The pulse loop **stays as the\n 84|daemon-side scheduler** (its thread, stop-flag, and drop-dir watching are\n 85|correct); what changes is what a fire DOES: each pulse/event invokes **one\n 86|bounded per-event turn** through the existing stack —\n...\n 102|`psyche_host_error` (kept name; it is already the harness-reachable pattern) +\n*103|cooldown, reset on a successful turn. No respawn storm exists anymore because\n*104|there is nothing resident to respawn.\n 105|\n 106|### W2 — psyche-own sid custody\n 107|\n...\n 114|  psyche's conversational thread survives parent resets (that is its job).\n*115|- `resume_psyche(ResumeMode::…)` validates the custody key before spawn\n 116|  (already implemented — resume.rs:183).\n 117|- **Reseed path:** when the psyche's own session is lost/invalid (first host,\n 118|  store wipe, harness-side expiry), fall back to\n...\n 120|  role/live/project into the psyche-download file whose path fills\n*121|  `{psyche_context_file}` (file-always; non-empty content = fresh preload,\n 122|  0-byte = continue — replaces the former `{psyche_context}` body key) and a\n 123|  fresh psyche session seeds from it. Loud marker on reseed (`PSYCHE_RESEED:{id}`) so a\n 124|  custody-loss loop is visible, plus the W1 failure budget bounds it.\n...\n 160|\n*161|Remove the own-copy spawn path for psyche roles (spawn from the registry\n 162|install dir like every other bounded adapter invocation — image-lock exposure\n 163|is now turn-bounded, same class as `translate`/`digest` today). GC existing\n 164|`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n...\n 225|  0.13.2): psyche failure of ANY shape must not remove/alter the parent\n*226|  endpoint's ready/hosted state, and hosting must not churn-respawn.\n 227|  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n 228|  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229|- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n## F-035-CONFLICT.md\n 34|\n*35|For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n 36|idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37|\n 38|| carrier | `active_only` to spt-hosted? |\n## FAULT-MATRIX.md\n 3|**Why this exists.** ADR-0002 collapsed networking into the one per-machine daemon\n*4|(SERIOUS #6, Stage-A red team): a networking bug and a PTY bug now share a failure\n 5|domain. The collapse is locked — the owed mitigation is this matrix: inject a fault\n 6|into each subsystem **independently**, observe the blast radius (what else breaks)\n 7|and the recovery (what brings it back), and grow the proven rows until every\n...\n 19||---|---|---|---|---|---|\n*20|| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\n*21|| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\n*22|| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\n*23|| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\n*24|| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\n 25|| 6 | **Registry feed: stale/replayed update over the wire** | None — update dropped | Stored registry state (newer epoch holds) | None needed (epoch lease absorbs it) | `tests/replicate.rs::registries_converge_over_the_wire_and_the_lease_holds`; `replicate.rs` unit tests |\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*32|| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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...\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 48|| 28 | **Untrusted origin injects notif records** (unpaired peer, or a `home`-trusted peer spoofing into `work`) | None — dropped fail-closed at the apply gate, zero rows written; the gate's subject is the handshake-proven stream-table origin, never payload bytes (KH 7.5) | The spool (nothing materialized); every subnet the origin isn't trusted in (trust is per-subnet) | None needed; pair the node to entitle it | `tests/notifsync.rs` (gate-negative leg); `notifsync.rs::untrusted_and_cross_subnet_origin...\n 49|| 29 | **Notif record for a non-member subnet** (a peer feeds rows for a subnet this node never joined) | None — dropped fail-closed; the subnet is never materialized locally (the REQ-INST-13 posture: unconfigured replicates nowhere) | The member subnets' spools; the feed (other records still apply) | None needed | `notifsync.rs::non_member_subnet_record_never_materializes` |\n 50|\n...\n 54||---|---|---|---|---|\n*55|| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n*56|| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57|| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58|| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n 59|| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60|| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61|| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n## FLAKE-LEDGER.md\n 9||---|------|-------------|-----------|--------|\n*10|| 1 | `spt-daemon brain_restart_survives_gaplessly` | 1× hosted Windows (2026-06-03, hung) | ConPTY stall under hosted-runner load | job `timeout-minutes` bounds it; not seen self-hosted |\n*11|| 2 | `spt-term digest_parses_a_real_pty_stream` | 3× gravity (2026-06-03; 2026-06-04 runs 26957386850, 26966738231) | sprint-collapse assert (`digest.rs`) — PTY input echo and the child's write are **concurrent writers** into the master-read stream; back-to-back `send_line`s let a later line's echo interleave mid-`Write(…)`, breaking the tool regex for that turn | HARDENED (2026-06-04): sends serialized — each line pumped back (adaptive echo-copy count, 2s/line bound) before the next, so no concu...\n 12|| 3 | `spt-daemon sync.rs torn_pull` + `concurrent_writes` | 1× (2026-06-03) | `wait_for_stream` deadline too tight under load | HARDENED: `wait_for_stream_except` 2s→10s; no recurrence |\n\n[Showing lines 1-336 of 706 (49.6KB limit). Use :337 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"tests, src, docs, crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":208,"fileCount":20,"files":["docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/CONTEXT-MEMORY.md","docs/DEBUG-CONVERGE-PLAN.md","docs/DEBUG-ROLLOUT.md","docs/DEFERRED.md","docs/DOCS-STRATEGY.md","docs/DORMANCY-BUDGET.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","docs/F-035-CONFLICT.md","docs/FAULT-MATRIX.md","docs/FLAKE-LEDGER.md","docs/GATEWAY-LIVENESS-DISPATCH.md","docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/MSG-IDENTITY-DISPATCH.md","docs/MUSL-W3-DISPATCH-RULING.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md"],"fileMatches":[{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":20},{"path":"docs/CONTEXT-MEMORY.md","count":1},{"path":"docs/DEBUG-CONVERGE-PLAN.md","count":1},{"path":"docs/DEBUG-ROLLOUT.md","count":3},{"path":"docs/DEFERRED.md","count":19},{"path":"docs/DOCS-STRATEGY.md","count":1},{"path":"docs/DORMANCY-BUDGET.md","count":8},{"path":"docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","count":20},{"path":"docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","count":12},{"path":"docs/F-035-CONFLICT.md","count":1},{"path":"docs/FAULT-MATRIX.md","count":18},{"path":"docs/FLAKE-LEDGER.md","count":8},{"path":"docs/GATEWAY-LIVENESS-DISPATCH.md","count":1},{"path":"docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","count":8},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/MANIFEST.md","count":20},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":6},{"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":20}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n *1│# Broker/Brain Split Restoration — Design Rationale (pre-ADR)\n  2│\n *3│**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` — see §0); six amendments folded in (marked **[V1]**–**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 — restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...\n  4│\n  5│**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.\n  6│\n   │...\n 12│\n*13│**Verdict: the core holds.** Confirmed at source — broker is an in-process thread (no broker child-spawn exists anywhere in the repo); no doc/plan/commit marks the collapse deliberate (silent drift; regression framing correct); `update.rs:233-234`'s \"exec the new binary's brain\" is aspirational and never wired; `applyhost.rs:176` records `applied` before the handoff (the optimistic `applied.json` watched on `enlyzeam`); `applyhost.rs:238-239` re-attaches `sessions.first()` with `from_seq=0` (both Q6 ga...\n 14│\n*15│**Two audit corrections folded in:** the NetHost is **already broker-owned** (`broker.rs:175` `OnceLock`, per the §B M4-D4a note), so the Q5 net item is near-free, not a migration; the supervise-backoff is wired at `peerloop.rs:805` (M8-D4), confirming Q7's reuse claim.\n 16│\n 17│**Six amendments raised and accepted** — each folded into the relevant section below and marked **[V1]**–**[V6]**.\n 18│\n   │...\n 34│\n*35│1. The hot-swap that *is* wired into `apply` is a **per-session brain handoff** (`apply_brain_only` → `Brain::handoff`), whose job is endpoint survival (REQ-UPD-3), not running-process replacement.\n*36│2. By explicit design, `applyhost.rs:36-38` states: *\"The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary); that restart is routine lifecycle, never an endpoint cycle.\"*\n 37│3. So new code only goes live on a *subsequent, unrelated* daemon restart (next logon, or a manual restart — which is what made the *other* node, `hfenduleam`, show a new pid). The practical consequence on `enlyzeam`: `gerald`'s live daemon kept running 0.3.0 — including the message-envelope codec — so the box from the original bug report **was still reproducing the original `\\r` corruption even though the fix sat on disk.**\n 38│\n 39│The operator then asked the real question this document answers: **what would it take to make an update actually hand off to the new executable seamlessly — no dropped processes, internal loop timings consistent across the swap, plus any other angles that matter?**\n   │...\n 48│\n*49│ADR-0004 (*\"Single consolidated daemon with broker/brain split; peer-propagated gated self-update\"*, **accepted 2026-05-29**) decided a **two-layer split implemented as two processes**:\n 50│\n*51│- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n*52│- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53│\n 54│Explicit evidence the split was meant to be a **process** boundary:\n*55│- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n*56│- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n*57│- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n*58│- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n 59│\n 60│### 2.2 What production actually does\n 61│\n*62│The production daemon runs the broker as a **background thread inside the single `spt daemon` process**:\n 63│\n*64│- `daemon.rs:41-44` — *\"bind the broker (served on a background thread) + the seed-control channel (the foreground loop).\"*\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 70│\n   │...\n 72│\n*73│- 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. So **the no-endpoint-drop self-update pillar is silently unrealized.**\n*74│- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that \"the live daemon execs the verified new binary\" is **aspirational; it was never wired** because the process split it depends on does not exist in production.\n 75│- Updates do not run until an unrelated restart/logon (observed live, Section 1).\n 76│\n 77│This document's decisions restore ADR-0004's intended model.\n 78│\n*79│> **Glossary note already applied:** `CONTEXT.md` was sharpened during the session to state explicitly that there is **one broker per machine** (per `SPT_HOME`), *not* one per endpoint — a single broker hosts every endpoint's resources and is present even with zero endpoints online. This corrected a latent \"one broker per endpoint\" misconception and is load-bearing for decision Q2.\n 80│\n 81│---\n 82│\n## CONTEXT-MEMORY.md\n 36│\n*37│A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-cl...\n 38│\n 39│### Ownership\n 40│\n## DEBUG-CONVERGE-PLAN.md\n 69│| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |\n*70│| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |\n 71│| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |\n 72│\n 73│Convergence = every expected node is `Applied`. Any node still pending when the\n## DEBUG-ROLLOUT.md\n  24│  known-good binary.\n *25│- A broker-touching candidate may be staged, but apply refuses it while\n *26│  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n  27│  first flow does not auto-cycle them.\n  28│\n  29│## One-time lab setup\n    │...\n 107│  current debug-channel version.\n*108│- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n 109│  resources; quiesce hosted endpoints first.\n 110│\n 111│## Agent checklist\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 15│| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n 16│| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n*17│| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n 18│| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19│| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20│| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n*21│| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n 22│| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-u...\n 23│| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner...\n*24│| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8...\n 25│| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruni...\n 26│| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27│| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n 28│| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\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   │...\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*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## DOCS-STRATEGY.md\n 123│`Messaging → Lifecycle → Terminal → Networking → Harness contract → Instances → Shells → Self-update`\n*124│(messaging is the primitive the quickstart needs; self-update last because its no-termination invariant depends on the terminal/broker model.)\n 125│**Per-vertical internal template (Django labels × Cloudflare ordering):**\n 126│`Overview (why+diagram) · Quickstart/Tutorial · How-to guides · Reference (rustdoc+CLI+schema) · llms.txt`\n 127│\n## DORMANCY-BUDGET.md\n 15│\n*16│1. binds a real (net-less) broker — the production PTY-hosting path — in a\n 17│   hermetic `SPT_HOME`;\n*18│2. spawns N idle seats through `Brain::spawn_session_pid` (one probe brain per\n 19│   seat, each labeled `budget-<i>`), exactly the shape of a **dormant** seat:\n*20│   PTY master + harness child + output ring, state-preserved and undriven;\n 21│3. settles, then samples each child's **RSS**, **handle/fd count**, and\n 22│   **cumulative CPU** twice across an idle window — the CPU delta is the idle\n 23│   burn of a warm seat;\n*24│4. samples the host process's RSS before/after spawning (the broker-side\n*25│   per-seat overhead: PTY plumbing + output rings);\n 26│5. kills every seat (the **suspended** state — only the on-disk perch record\n 27│   would remain in production) and samples the residual.\n 28│\n   │...\n 31│count, and `/proc/<pid>/stat` utime+stime. **Limitation:** the sample covers\n*32│the direct PTY child only — a harness that forks a worker tree under-reports\n 33│by the tree's cost (the claude numbers below are the main node process, which\n 34│dominates).\n 35│\n   │...\n 61│\n*62│Broker-side overhead: ~0.2–0.3 MiB per seat (RSS delta ÷ N). Suspended\n 63│residual: host process returns to its ~6–7 MiB baseline — a suspended seat\n 64│costs **only the on-disk perch record** (bytes, not megabytes, no handles, no\n 65│CPU).\n   │...\n 78│\n*79│Broker-side overhead: ~0.3 MiB per seat. Suspended residual: host process\n 80│back at its ~4.7 MiB baseline.\n 81│\n 82│## The policy (locked by these numbers)\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n *1│# F-027 — ENDPOINT_SPAWN_FAIL: pre-spawn seam + bind-deadline honesty (DESIGN)\n  2│\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  8│\n *9│Scope split: **Half A (bind-deadline honesty) and Half B (pre-spawn manifest\n 10│seam) are spt-core changes** (todlando's wave, sequenced after F-029). **The\n 11│trust-seeder binary itself is adapter-side** (perri's wave — spt-core stays\n 12│harness-agnostic per the glue-model boundary: adapters = manifest + binary only;\n   │...\n 18│\n*19│`spt endpoint run <id> --start` prints `ENDPOINT_RUN_STARTED` when the broker\n*20│has accepted the spawn RPC and a child PID exists — **nothing more is verified**\n 21│(cli.rs:1819 machine line, cli.rs:1833 `--start` success print). A harness that\n*22│launches but wedges post-spawn (workspace-trust prompt, null stdin) never fires\n 23│its SessionStart hook, so `establish_perch` never runs (api/startup.rs:299 is\n 24│the ONLY writer of the `ready` marker), and the endpoint sits `ready=false`\n 25│**forever, silently** — while the operator was told it started.\n   │...\n 28│cli.rs:2735/3826) but nothing gates or alarms on it: passive display only. The\n*29│attach path's `await_endpoint_session` (cli.rs:1854, 25 s) gates on broker\n*30│**session existence** — created synchronously at spawn, so it passes instantly\n 31│for a wedged harness — and the `--start` path skips even that.\n 32│\n*33│Precedent trap to avoid: `TRANSLATION_SPAWN_FAILED` (broker.rs:1271) is loud on\n 34│the **detached daemon's stderr, which nobody reads** (ADR-0022:130,\n 35│KNOWN-HAZARDS F-019 lesson). Any F-027 token must land on **CLI stderr and/or\n 36│the perch record**, never daemon-stderr-only.\n   │...\n 42│| 1 | TRUST-WEDGE — CC at workspace-trust dialog, null stdin; `ENDPOINT_RUN_STARTED` printed; ready=false forever | every fresh probe cwd, this session | SILENT |\n*43│| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n*44│| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n*45│| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n*46│| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n*47│| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n*48│| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n 49│\n 50│Seed-1 correction (accepted): the \"backslash-keyed `hasTrustDialogAccepted`\n 51│silently ignored\" pin from the RUNTRUTH triage was a **rig artifact** (hand-\n   │...\n 57│Seed-2 (the deeper root): trust is stored **per config root**, and the root a\n*58│spawn resolves is a **function of whether `CLAUDE_CONFIG_DIR` survives the\n*59│spawn env-rebuild** — which differs by spawn mode (detached daemon\n*60│`CreateEnvironmentBlock` rebuild vs foreground inherit). Two spawns on one box\n 61│resolved different accounts (Reavo vs Brandon). Seeding the operator shell's\n 62│root is therefore WRONG in general.\n 63│\n   │...\n 67│The silent class (modes 1, 2, 3, 6) must fail with the same loudness as the\n*68│pre-spawn class (modes 4, 5) already does.\n 69│\n 70│### Half A — bind-deadline honesty (spt-core, harness-agnostic)\n 71│\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n  20│\n *21│- `livehost.rs:497 → host_one:518` spawns the psyche\n *22│  (`spawn_psyche_owned(Some(&session_id), None)`) and starts `run_pulse_loop`\n  23│  as its driver; `confirm_residency_or_unhost`\n  24│  (REQ-HAZARD-LIVEHOST-NONRESIDENT) probes the owned child and re-hosts every\n  25│  ~5 s reconcile tick when it isn't resident.\n    │...\n  27│  production callers**: `resume_psyche` (spt-live resume.rs:175, mode-validated\n *28│  spawn + nested rebind), `run_psyche_turn` (spt-live turn.rs:72, bounded\n  29│  stdin→stdout turn; its only production caller is the ADR-0013 conflict-merge\n  30│  in reconcile.rs), and `psyche_turn_and_relay` (spt-daemon psyrelay.rs:168,\n  31│  turn + intent-parse + sanitized relay).\n    │...\n  70│| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |\n *71│| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |\n  72│| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |\n  73│| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |\n  74│| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |\n    │...\n  80│\n *81│### W1 — wire the per-event driver (retire the resident spawn)\n  82│\n *83│`host_one` stops spawning a resident psyche. The pulse loop **stays as the\n  84│daemon-side scheduler** (its thread, stop-flag, and drop-dir watching are\n  85│correct); what changes is what a fire DOES: each pulse/event invokes **one\n  86│bounded per-event turn** through the existing stack —\n    │...\n 102│`psyche_host_error` (kept name; it is already the harness-reachable pattern) +\n*103│cooldown, reset on a successful turn. No respawn storm exists anymore because\n*104│there is nothing resident to respawn.\n 105│\n 106│### W2 — psyche-own sid custody\n 107│\n    │...\n 114│  psyche's conversational thread survives parent resets (that is its job).\n*115│- `resume_psyche(ResumeMode::…)` validates the custody key before spawn\n 116│  (already implemented — resume.rs:183).\n 117│- **Reseed path:** when the psyche's own session is lost/invalid (first host,\n 118│  store wipe, harness-side expiry), fall back to\n    │...\n 120│  role/live/project into the psyche-download file whose path fills\n*121│  `{psyche_context_file}` (file-always; non-empty content = fresh preload,\n 122│  0-byte = continue — replaces the former `{psyche_context}` body key) and a\n 123│  fresh psyche session seeds from it. Loud marker on reseed (`PSYCHE_RESEED:{id}`) so a\n 124│  custody-loss loop is visible, plus the W1 failure budget bounds it.\n    │...\n 160│\n*161│Remove the own-copy spawn path for psyche roles (spawn from the registry\n 162│install dir like every other bounded adapter invocation — image-lock exposure\n 163│is now turn-bounded, same class as `translate`/`digest` today). GC existing\n 164│`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n    │...\n 225│  0.13.2): psyche failure of ANY shape must not remove/alter the parent\n*226│  endpoint's ready/hosted state, and hosting must not churn-respawn.\n 227│  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n 228│  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229│- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n## F-035-CONFLICT.md\n 34│\n*35│For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n 36│idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37│\n 38│| carrier | `active_only` to spt-hosted? |\n## FAULT-MATRIX.md\n  3│**Why this exists.** ADR-0002 collapsed networking into the one per-machine daemon\n *4│(SERIOUS #6, Stage-A red team): a networking bug and a PTY bug now share a failure\n  5│domain. The collapse is locked — the owed mitigation is this matrix: inject a fault\n  6│into each subsystem **independently**, observe the blast radius (what else breaks)\n  7│and the recovery (what brings it back), and grow the proven rows until every\n   │...\n 19│|---|---|---|---|---|---|\n*20│| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\n*21│| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\n*22│| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\n*23│| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\n*24│| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\n 25│| 6 | **Registry feed: stale/replayed update over the wire** | None — update dropped | Stored registry state (newer epoch holds) | None needed (epoch lease absorbs it) | `tests/replicate.rs::registries_converge_over_the_wire_and_the_lease_holds`; `replicate.rs` unit tests |\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*32│| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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   │...\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 48│| 28 | **Untrusted origin injects notif records** (unpaired peer, or a `home`-trusted peer spoofing into `work`) | None — dropped fail-closed at the apply gate, zero rows written; the gate's subject is the handshake-proven stream-table origin, never payload bytes (KH 7.5) | The spool (nothing materialized); every subnet the origin isn't trusted in (trust is per-subnet) | None needed; pair the node to entitle it | `tests/notifsync.rs` (gate-negative leg); `notifsync.rs::untrusted_and_cross_subnet_origin...\n 49│| 29 | **Notif record for a non-member subnet** (a peer feeds rows for a subnet this node never joined) | None — dropped fail-closed; the subnet is never materialized locally (the REQ-INST-13 posture: unconfigured replicates nowhere) | The member subnets' spools; the feed (other records still apply) | None needed | `notifsync.rs::non_member_subnet_record_never_materializes` |\n 50│\n   │...\n 54│|---|---|---|---|---|\n*55│| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n*56│| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57│| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58│| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n 59│| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60│| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61│| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n## FLAKE-LEDGER.md\n  9│|---|------|-------------|-----------|--------|\n*10│| 1 | `spt-daemon brain_restart_survives_gaplessly` | 1× hosted Windows (2026-06-03, hung) | ConPTY stall under hosted-runner load | job `timeout-minutes` bounds it; not seen self-hosted |\n*11│| 2 | `spt-term digest_parses_a_real_pty_stream` | 3× gravity (2026-06-03; 2026-06-04 runs 26957386850, 26966738231) | sprint-collapse assert (`digest.rs`) — PTY input echo and the child's write are **concurrent writers** into the master-read stream; back-to-back `send_line`s let a later line's echo interleave mid-`Write(…)`, breaking the tool regex for that turn | HARDENED (2026-06-04): sends serialized — each line pumped back (adaptive echo-copy count, 2s/line bound) before the next, so no concu...\n 12│| 3 | `spt-daemon sync.rs torn_pull` + `concurrent_writes` | 1× (2026-06-03) | `wait_for_stream` deadline too tight under load | HARDENED: `wait_for_stream_except` 2s→10s; no recurrence |\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*23│| 14 | `spt::bin rc::tests::attach_viewport_reconnects_across_a_broker_bounce` | 1× hfenduleam local gate (2026-07-06, WORKER-TRUTH W-3 build @worker-truth, `nextest -p spt --bin spt` full-parallel 360-test run) | **TIMEOUT, not an assertion fail** — SLOW>60…>TERMINATING at the 240s cap under the 360-way `--bin spt` pool. A REAL-broker + broker-bounce reconnect UNIT test (rc.rs:2071) that escaped the HEAVY-class serialization: the Phase-A/B split + `heavy-broker-pty` test-group target integration te...\n## GATEWAY-LIVENESS-DISPATCH.md\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 11│The adapter's suspend-honor poll reads `--json` (`parse_endpoint_status` over `endpoint list --json --show-all`) → sees Suspended → self-suspends the gateway, despite pid-alive + human-ONLINE.\n 12│\n## INJECT-SETTLE-REARM-GATE-VERDICT.md\n  5│## Scope\n *6│Post-0.29.0 field bug: PTY inject HEAD-TRUNCATION recurred on 0.29.0 — a checkpoint-wake payload injected right after `/clear` lost its head (perri screenshot `WindowsTerminal_6iSjya8pMt.png`). doyle root-cause + design ruling; todlando build.\n  7│\n *8│**Root:** `run_inject_worker` gated the W5-A Layer-1 settle-gate behind a worker-local ONE-SHOT (`settled_once`) on a FALSE premise (head-swallow race is startup-only). A mid-session `/clear` re-enters the harness raw-mode input reader → re-creates the pre-settle window, but the one-shot already fired at spawn → `settle_before_inject` skipped → head eaten again; echo-verify (Layer 2) default-OFF for that session → silent + unrecoverable.\n  9│\n*10│**Fix:** `settled_once` one-shot → `probe_unobservable` latch + pure `should_settle(attempt, probe_unobservable) = attempt>1 || !probe_unobservable`. Re-settle before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY (bug-prone class, cheap); latch-skip the steady-state settle ONLY where the probe is UNOBSERVABLE (non-echoing ConPTY — no reattach race, full-deadline cost). The settle's own bool return discriminates the class, latched on attempt 1; a re-drive always settles. Mints REQ-HAZARD-I...\n 11│\n 12│## Diff review — FAITHFUL\n*13│Matches the design ruling exactly: latch semantics, pure gate, first-attempt-only latch update, re-drive-always-settles. KH 7.37 + traceable REQ minted with all three stages (`doc`/`impl`/`unit`) tagged on real evidence. RED-first unit `should_settle_rearms_on_observable_pty` present and correct. No int edit — safety-checked: `inject_control_wedge` mocks (`findstr`/`cat`) never answer the DSR probe → settle times out → `probe_unobservable` latches true after delivery 1 → identical to the old one-...\n 14│\n 15│## Both legs GREEN\n*16│- **Windows (ConPTY, isolated worktree + fresh target):** clippy `--workspace --all-targets -D warnings` = 0; `traceable-reqs check --json` = 0; nextest `--workspace` = **1707/1707** (9 leaky, 1 skip), exit 0. `should_settle_rearms_on_observable_pty` PASS.\n*17│- **Linux (kitsubito/forkpty, bundle-deploy @ddf7f8e):** clippy = 0; nextest `--workspace` = **1695/1695** (2 leaky, 1 skip), exit 0. `should_settle_rearms_on_observable_pty` PASS.\n 18│\n 19│## GATE-HOLD investigated → CLEARED (env contamination, not code)\n 20│First Windows full-suite run REDed on `inject_control_wedge::wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` (`pending_spool_rows: left=1 right=0`, `INJECT_HEAD_LOSS_SPOOL` fired). Discriminated:\n 21│- Deterministic 5/5 isolated FAIL → not a flake.\n*22│- Root: `SPT_INJECT_VERIFY_ECHO=1` was exported in the gater's interactive live-agent shell (the live box arms Layer-2 echo-verify), inherited by the gate script → `echo_capable=true` → on Windows ConPTY the choreo child puts a headless line on the ring → Layer-2 false-positive head-loss → respool → `pending=1`.\n 23│- Proven env-driven: same test PASSES with `SPT_INJECT_VERIFY_ECHO` unset (deterministic both ways). Linux passed because its ssh env is clean.\n*24│- **CI-safe:** the var is **Process-scope only** (User/Machine scope empty) — the GitHub Actions runner service (hfenduleam, separate process tree) does NOT inherit it → PR CI runs clean. The clean-env full re-run (1707/1707) = CI's actual condition.\n 25│- The fix touches only Layer-1 settle; Layer-2 echo-verify is untouched W5-A code. The failure is orthogonal to the change.\n 26│\n 27│## Follow-up SEED (non-blocking; filed to todlando)\n## KNOWN-HAZARDS.md\n   4│\n  *5│**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...\n   6│\n   7│---\n   8│\n    │...\n  47│- **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 *48│- **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  49│- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n  50│\n  51│### 2.2 Stdin session_id precedence over env\n    │...\n  57│### 2.3 Binary-handoff argv schema must stay backward-compatible\n *58│- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n  59│- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n *60│- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n  61│- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n  62│\n  63│### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n    │...\n  66│- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n *67│- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n  68│- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n  69│\n  70│### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n *71│- **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 `...\n  72│- **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** (`inf...\n  73│- **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 ...\n  74│- **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  80│### 3.1 Ephemeral perch cleanup on every `ring` exit path\n *81│- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\n  82│- **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  83│- **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  84│- **Sister cite:** `src/owl/ring.rs:58-294`.\n    │...\n  87│- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\n *88│- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\n  89│- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n  90│- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n  91│\n    │...\n 178│- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\n*179│- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\n 180│- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\n 181│\n 182│### 5.2 Windows EBUSY on atomic rename\n    │...\n 199│\n*200│### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\n*201│- **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*202│- **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*203│- **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*204│- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\n 205│\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 211│- **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 212│\n*213│### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n*214│<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n*215│- **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 ...\n## MANIFEST.md\n 100│<!-- [doc->REQ-INSTALL-11] -->\n*101│**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is t...\n 102│\n 103│```toml\n 104│# GO-LIVE GATE ONLY. Declaring [session.psyche_init] is the single signal that\n*105│# promotes the endpoint to a LiveAgent — but spt-core NEVER spawns it (there is no\n 106│# resident Psyche to \"init\"). The per-event turn drives [session.psyche_resume]\n 107│# EXCLUSIVELY (below). The command is opaque and, in the ephemeral model, unexecuted;\n 108│# its presence is the whole contract, so keep it minimal.\n    │...\n 123│<!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] -->\n*124│**Psyche contract — a bounded per-event turn (since v0.25.0).** A Psyche is **not** a resident process the daemon keeps alive between events. Declaring `[session.psyche_init]` is a **go-live gate only** — its presence promotes the endpoint to a LiveAgent, but spt-core **never spawns** it. Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) instead runs **exactly one bounded turn** through **`[session.psyche_resume]` — the sole driven psyche role** — spa...\n 125│\n 126│<!-- [doc->REQ-PSYCHE-SID-CUSTODY] -->\n 127│**Custody sid — `{session_id}` is the Psyche's OWN id.** In a psyche role template `{session_id}` is the **Psyche's own minted session id**, kept in its nested `<parent>-psyche` perch record — **not** the parent's. A parent boundary (`/clear`, `/compact`) rotates the *parent's* sid but does **NOT** rotate the psyche sid: the Psyche's conversational thread survives parent resets (that is its job). When a template still needs the parent's sid it takes the **explicit** `{parent_session_id}` key — neve...\n    │...\n 129│<!-- [doc->REQ-PSYCHE-CONTEXT-FILE-INDIRECTION] -->\n*130│**Psyche-download — `{psyche_context_file}` (file-always, replaces `{psyche_context}`).** The composed Psyche mind rides a **file**, never the command argv: before each turn spt-core writes the mind into the nested psyche perch dir and fills a single **`{psyche_context_file}` = that path** (argv-cap-immune — a real ~20 KB mind exceeds the win32 command-line cap and would brick the spawn). The soft **fresh-vs-continue** discriminator is the file's **content**, not key presence: a **fresh** (first / re...\n 131│\n 132│<!-- [doc->REQ-PSYCHE-NESTED-RESOLUTION] -->\n 133│**`{subnet}` — the endpoint's home-subnet label.** A single `{subnet}` key fills to this endpoint's home-subnet label (`local` when unhomed), supplied whenever it is known so a psyche turn need never resolve a `--subnet` it cannot know; when no subnet is known the key is left unfilled so a referencing template fails **loud** (the `{node}` precedent). There is deliberately **no `{home}` key** — the endpoint home subnet is one concept, and `home:` is already the subnet-name qualifier position, so a `{h...\n    │...\n 135│<!-- [doc->REQ-PSYCHE-SID-CUSTODY] -->\n*136│**Reseed — reserved exit code 95.** When the Psyche's own session is lost or invalid (first host, store wipe, harness-side expiry) the adapter signals it with the **reserved exit code 95**; spt-core clears the stored custody sid and drives the next turn as a fresh preload (a non-empty `{psyche_context_file}`), emitting a loud `PSYCHE_RESEED:<id>` marker so a custody-loss loop is visible — the failure budget above bounds it.\n 137│\n 138│<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->\n 139│**`[session.resume]` — the agent's own-session NATIVE resume.** `self` is the\n    │...\n 151│harness resolves a transcript by `session_id` **+ cwd**, so spt-core also lands\n*152│the resumed PTY in the session's recorded **project cwd** — the per-session\n 153│ledger row's `cwd` → else the perch `info.cwd` → else the current dir (so a\n 154│single-project endpoint and pre-migration ledger rows still resume correctly).\n 155│\n    │...\n 162│\n*163│**Substitution-key catalog.** The full `{key}` vocabulary spt-core guarantees it can fill — a role's `keys` must be a subset (source of truth: `spt_runtime::SUBSTITUTION_KEYS`): `{id}`, `{adapter_name}`, `{adapter_dir}`, `{session_id}`, `{parent_session_id}`, `{session_name}`, `{node}`, `{subnet}`, `{parent_pid}`, `{agent_type}`, `{psyche_context_file}`, `{link_token}`, `{source}`. spt-core fills only the keys relevant to a given spawn (`{psyche_*}` for a live agent's Psyche role, `{source}` for a `[di...\n 164│\n 165│<!-- [doc->REQ-MANIFEST-SUBST] -->\n 166│**Adapter-static keys — `{adapter_dir}` and `{adapter_name}` (since v0.16.0).** Two of the catalog keys are *adapter-static* — they depend only on the resolved adapter, never on a session or event, so they are available **wherever** command/string substitution runs (every `[session.*]` template, the `[digest]` extractor, the `[message-idle-translation-binary].command`, and — uniquely — inside `[strings]` values at `get-string` read time):\n    │...\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 177│| Key | Fills to | Notes |\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 189│<!-- [doc->REQ-NOTIF-2] -->\n*190│`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.\n 191│\n 192│```toml\n 193│[session.notif]\n    │...\n 198│### `[env]` — env-var table\n*199│Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.\n 200│\n 201│```toml\n 202│[env.OWL_SESSION_ID]\n    │...\n 204│value = \"{session_id}\"\n*205│channel = \"CLAUDE_ENV_FILE\"        # harness-hosted only; spt-hosted inherits from broker\n 206│```\n 207│\n 208│**`direction = \"read\"` — capture a launch-env var for template substitution.**\n    │...\n 302│--sample <real-log>` runs it and prints the parsed records, the rendered digest,\n*303│and **every dropped line with its reason** — no silent empty.\n 304│\n 305│<!-- [doc->REQ-ADAPTER-PROOF-DIR-OVERRIDE] -->\n 306│To proof a **DEV build that is not registered** (a freshly built binary beside a\n    │...\n 322│<!-- [doc->REQ-MANIFEST-SUBST] -->\n*323│**Lazy substitution inside `[strings]` values (since v0.16.0).** A `[strings]` value (inline literal or the read-back contents of a pointer file) may contain the **adapter-static** placeholders `{adapter_dir}` / `{adapter_name}`; `get-string` substitutes them at read time and returns the resolved string. This is scoped to **those two keys only** — `get-string` carries no session context, so session-scoped keys (`{id}`, `{session_id}`, …) are **not** available here and an attempt to use one is left un...\n 324│\n 325│```toml\n 326│[strings]\n    │...\n 333│### `[inject]` — inject-input methods\n*334│Per activity-state, any combination: `pty`, `hook`, `relay`, `http`.\n 335│```toml\n 336│[inject]\n 337│activity = [\"hook\"]                # non-disruptive during work\n*338│idle     = [\"pty\", \"hook\"]\n 339│```\n 340│\n 341│<!-- [doc->REQ-MSG-IDLE-TRANSLATION-BINARY] -->\n    │...\n 343│Opt-in. The adapter's **idle-delivery translation binary**: a pure stdin→stdout\n*344│JSON-lines filter spt-core lifecycle-manages (spawned when the spt-hosted endpoint\n 345│comes up, terminated when it goes down). spt-core feeds it the inbound `<EVENT>`\n*346│feed and reads back keystroke-commands, which spt-core applies to the broker-held\n*347│PTY **atomically** — controller input is buffered during the emitted sequence and\n*348│flushed after, so injection coexists with a live `spt rc` controller (spt-core owns\n## MSG-IDENTITY-DISPATCH.md\n 21│- `REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE` [doc,impl,unit] — daemon scrubs inherited\n*22│  `SPT_ENDPOINT_ID`/`OWL_SESSION_ID`/`SPT_AGENT_ID` at startup + before EVERY role spawn\n 23│  (core defense; today only the role's declared `env_remove` is stripped, runtime.rs:728).\n*24│  Root: perri F-036 RCA — leaked `SPT_ENDPOINT_ID=doyle` through psyche_resume spawns rotated\n 25│  doyle's perch to psyche sids every pulse. Adapter half shipped v0.18.8.\n 26│  **Fold:** the leg-b docs fix — `recursion_guard_env` schema description is role-agnostic\n 27│  (manifest.rs:314 + crates/spt-runtime/manifest.schema.json:306, drop \"summarizer children\").\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 68│- `REQ-ENDPOINT-AUTOSTART` [doc,impl,unit,int] — **shape (a) RULED**: `spt endpoint run --save`\n*69│  persists a startup default replayed at daemon start (symmetric with shipped `subnet attach\n 70│  --save`). Shape (c) restore-what-was-up REJECTED for now (couples to the effective_rest_state /\n 71│  F-035 reader-parity neighborhood that just churned). Loud-skip on failed replay; never blocks\n 72│  daemon start. Deployah field-confirmed the gap live (mobile-gw alive=false after the v0.30.6\n   │...\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## MUSL-W3-DISPATCH-RULING.md\n 13│## Scope — release-pipeline data, NOT dependency work\n*14│`/diagnose` 2026-07-08 (backlog #14) proved the tree builds+runs static musl with ZERO source changes (rustls not openssl; aws-lc-sys + bundled-sqlite clean under `musl-gcc`; openpty; static DNS/HTTPS). So W3 is CI-matrix + assemble + sign + update-set wiring. Sites:\n 15│\n 16│1. **`release.yml` — musl matrix entry** (build on **kitsubito**, the proven musl toolchain box):\n 17│   - install in-job: `musl-tools` + `cmake` + `rustup target add x86_64-unknown-linux-musl`\n## NEXT-MILESTONE-BUG-TRIAGE.md\n  11│- **C — CLI / picker UX:** #1, #5, #11, #13\n *12│- **D — PTY / terminal rendering:** #6, #7, #8, #12, #14\n  13│\n  14│---\n  15│\n    │...\n  23│### A-#3 — cross-node \"controlled\" state not gossiping  · STATUS: TRIAGE\n *24│Anecdote: `ball-b` on enlyzeam is **attached/controlled**, yet sceltouin `spt endpoint run` shows it **\"online, ready to control\"** (see image: status ONLINE, no controlled marker). W5 added `controller_node` gossip — either not populated on the controlling node or not read on the remote viewer. Operator idea: on selecting a remote endpoint, run a fresh `spt node-refresh <node-id>` to pull current gossip before acting.\n  25│- Root cause: _TBD_\n  26│- Fix approach: _TBD_\n  27│\n    │...\n  37│### B-#4 — cross-node online endpoints not detected by `spt rc`  · STATUS: TRIAGE\n *38│`spt endpoint list` shows `ball-b ENLYZEAM Active`, but `spt rc ball-b` → `RC_FAIL:ball-b: no live session for endpoint 'ball-b'`. rc resolves only LOCAL live sessions; a cross-node Active endpoint isn't reachable via rc (no remote-attach path wired, or the resolver doesn't route to the owning node).\n  39│- Root cause: _TBD_\n  40│- Fix approach: _TBD_\n  41│\n    │...\n  71│\n *72│## Cluster D — PTY / terminal rendering\n  73│\n  74│### D-#6 — scrolling broken for CC (Claude Code TUI)  · STATUS: TRIAGE\n *75│Everything above the CC TUI content shows raw terminal history (scrollback corrupt). Proposal: broker uses win32 vterm? Acceptable that Ctrl+B breaks if non-SPT PTY features all work.\n *76│- Root cause: _TBD (broker PTY scrollback / alt-screen handling)_\n  77│- Fix approach: _TBD_\n  78│\n *79│### D-#7 — PTY clutter as the TUI animates/scrolls  · STATUS: TRIAGE\n  80│Residual characters left in whitespace as the TUI animates (image: stray chars). Possibly related to the new top-right ID marker injection.\n  81│- Root cause: _TBD_\n  82│- Fix approach: _TBD_\n    │...\n 113│\n*114│**C-#11 endpoint-list palette parity** · conf H · effort M · COUPLED #13. ROOT: `format_subnet_rows` (cli.rs:2529) prints raw `{:?}` status (2538), no color/glyph; picker derives from SAME ResourceRow: Status→EpStatus (data.rs:273-278), bound/controller/harness (289-323), display_status()→EpDisplay (model.rs:292), square_span (view.rs:64)+display_color (view.rs:28). Shared input = ResourceRow (registry.rs:383). BLOCKER: picker submods private (picker/mod.rs:18-21). FIX: (1) pub EpStatus/EpDisplay/...\n 115│\n 116│**C-#13 subnet tab dup per subnet** · conf H · effort M · COUPLED #11. ROOT: `subnet_rows` (data.rs:253-331) iterates PER subnet, pushes row per (subnet,endpoint), `group=\"{subnet}:{node}\"` (298), no cross-subnet dedup → machine in 2 subnets = 2 headers (view.rs:268-276). Dedup key = row.node (node pubkey, 1/machine)+endpoint_id (ResourceRow lacks machine_id). FIX: (1) data.rs map keyed (node,endpoint_id) across subnet loop, collect subnet-name SET, emit 1 row/machine-endpoint group=node_display, re...\n 117│\n    │...\n 119│\n*120│## Cluster D — PTY/terminal rendering (DIAGNOSED)\n 121│\n*122│**ID-marker spine (#7/#8/#14):** it's `StatusRow`/REQ-RC-IDENTITY (v0.16.0 W6), CLIENT-side in rc.rs:198-307 (NOT broker). Paints `{subnet}:{id}@{node}` right-aligned cyan on real terminal row 1 via DECSTBM scroll-region `ESC[2;{rows}r` + DECSC `\\x1b7`/home/`\\x1b[2K`/right-align/cyan/DECRC `\\x1b8` — ABSOLUTE one-shot. Repainted only on: attach (1297-1302), ReassertScanner alt-screen/DECSTBM-reset hit (1413-1419), polled size-change (1329-1349). NOT on ordinary scroll/redraw.\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 131│\n*132│**D-#14 marker starts off-screen** · conf M-H · effort S-disable · coupled #7/#8 · cross-platform. ROOT: same StatusRow one-shot; cold attach paints row 1 but full-ring replay (#6)+scroll push it into scrollback, never re-stickies. REC: **ship DISABLED behind a feature flag now** — net-negative until #6 grid enables a proper per-frame sticky top-row repaint.\n 133│\n*134│**D sequencing:** (1) flag the ID-marker OFF now → cheaply fixes regression-half of #7/#8 + all #14. (2) #12 independent + cheap (VT-out enable) → do regardless. (3) #6 server-side grid = foundational, real fix for #6 + prereq for redoing marker as sticky overlay (#8/#14) + killing residual artifacts (#7). Key files: rc.rs (StatusRow/RawGuard/pump), broker.rs (ring+replay), attach.rs (from_seq=0), spt-term/{lib,stream,pty}.rs.\n 135│\n 136│## Cluster B — cross-node messaging/routing (DIAGNOSED)\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    │...\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 146│## Cluster A — registry gossip & presence (DIAGNOSED)\n 147│\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    │...\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    │...\n 171│\n*172│### E-#16 — eel-a (sole live endpoint, hfenduleam) can no longer be attached  · STATUS: TRIAGE\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\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  8│`required_stages=[]` first, activate per wave as started** (traceable rule 5).\n   │...\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 33│Root: `psyche_host_error` clears only via the pulse-loop `note_turn_outcome` Ok leg\n   │...\n 48│\n*49│## W2 — broker viewer/brain decouple (flagship — the update wedge)\n 50│\n*51│**REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE** (doc, impl, unit, int)\n*52│Root, rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): PTY viewer fan-out\n*53│and control mutations depend synchronously on a live, DRAINING brain — brain-subscriber\n*54│session-output writes ride UNDER the per-session log lock (broker.rs:19-20). Failed writes are\n*55│handled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within\n*56│seconds: attached rc output freezes; detach does NOT release the control stamp (release routes\n*57│through the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays\n*58│healthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled\n*59│or slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\n*60│wedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply \"promoted\"\n 61│while frozen.\n 62│Fix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log\n*63│lock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already\n*64│buffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control\n*65│stamp release/take must complete against the BROKER without brain round-trip (or bounded with\n 66│loud timeout). doc = KNOWN-HAZARDS entry.\n*67│Int (the rig, encoded): suspend the brain child mid-session → attached viewer ticks CONTINUE +\n 68│`rc --take` completes; resume → no output lost (cursor replay).\n 69│\n 70│## W3 — update finish truth (the \"seamless live update\" cornerstone)\n   │...\n 77│`start-reason=daemon-restart`; agents' minds ride psyche re-host as today.\n*78│Int: endpoint online → daemon stop → start → endpoint back ONLINE, same id, harness respawned.\n 79│\n 80│**REQ-UPDATE-ONE-SHOT-FINISH** (impl, unit)\n 81│Operator wart: `update fetch`/`update apply` run `ensure_daemon_announced` (cli.rs:4386) →\n*82│on a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain\n 83│pair + a manual bounce. Fix: apply works daemonless (swap + record, next start runs new bytes);\n*84│`update apply --finish` (name subject to docs-token gate) completes the cycle: swap → brain","missingPaths":["test","packages"],"truncation":{"content":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n*1|# Broker/Brain Split Restoration — Design Rationale (pre-ADR)\n 2|\n*3|**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` — see §0); six amendments folded in (marked **[V1]**–**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 — restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...\n 4|\n 5|**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.\n 6|\n...\n 12|\n*13|**Verdict: the core holds.** Confirmed at source — broker is an in-process thread (no broker child-spawn exists anywhere in the repo); no doc/plan/commit marks the collapse deliberate (silent drift; regression framing correct); `update.rs:233-234`'s \"exec the new binary's brain\" is aspirational and never wired; `applyhost.rs:176` records `applied` before the handoff (the optimistic `applied.json` watched on `enlyzeam`); `applyhost.rs:238-239` re-attaches `sessions.first()` with `from_seq=0` (both Q6 ga...\n 14|\n*15|**Two audit corrections folded in:** the NetHost is **already broker-owned** (`broker.rs:175` `OnceLock`, per the §B M4-D4a note), so the Q5 net item is near-free, not a migration; the supervise-backoff is wired at `peerloop.rs:805` (M8-D4), confirming Q7's reuse claim.\n 16|\n 17|**Six amendments raised and accepted** — each folded into the relevant section below and marked **[V1]**–**[V6]**.\n 18|\n...\n 34|\n*35|1. The hot-swap that *is* wired into `apply` is a **per-session brain handoff** (`apply_brain_only` → `Brain::handoff`), whose job is endpoint survival (REQ-UPD-3), not running-process replacement.\n*36|2. By explicit design, `applyhost.rs:36-38` states: *\"The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary); that restart is routine lifecycle, never an endpoint cycle.\"*\n 37|3. So new code only goes live on a *subsequent, unrelated* daemon restart (next logon, or a manual restart — which is what made the *other* node, `hfenduleam`, show a new pid). The practical consequence on `enlyzeam`: `gerald`'s live daemon kept running 0.3.0 — including the message-envelope codec — so the box from the original bug report **was still reproducing the original `\\r` corruption even though the fix sat on disk.**\n 38|\n 39|The operator then asked the real question this document answers: **what would it take to make an update actually hand off to the new executable seamlessly — no dropped processes, internal loop timings consistent across the swap, plus any other angles that matter?**\n...\n 48|\n*49|ADR-0004 (*\"Single consolidated daemon with broker/brain split; peer-propagated gated self-update\"*, **accepted 2026-05-29**) decided a **two-layer split implemented as two processes**:\n 50|\n*51|- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n*52|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53|\n 54|Explicit evidence the split was meant to be a **process** boundary:\n*55|- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n*56|- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n*57|- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n*58|- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n 59|\n 60|### 2.2 What production actually does\n 61|\n*62|The production daemon runs the broker as a **background thread inside the single `spt daemon` process**:\n 63|\n*64|- `daemon.rs:41-44` — *\"bind the broker (served on a background thread) + the seed-control channel (the foreground loop).\"*\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 70|\n...\n 72|\n*73|- 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. So **the no-endpoint-drop self-update pillar is silently unrealized.**\n*74|- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that \"the live daemon execs the verified new binary\" is **aspirational; it was never wired** because the process split it depends on does not exist in production.\n 75|- Updates do not run until an unrelated restart/logon (observed live, Section 1).\n 76|\n 77|This document's decisions restore ADR-0004's intended model.\n 78|\n*79|> **Glossary note already applied:** `CONTEXT.md` was sharpened during the session to state explicitly that there is **one broker per machine** (per `SPT_HOME`), *not* one per endpoint — a single broker hosts every endpoint's resources and is present even with zero endpoints online. This corrected a latent \"one broker per endpoint\" misconception and is load-bearing for decision Q2.\n 80|\n 81|---\n 82|\n## CONTEXT-MEMORY.md\n 36|\n*37|A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-cl...\n 38|\n 39|### Ownership\n 40|\n## DEBUG-CONVERGE-PLAN.md\n 69|| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |\n*70|| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |\n 71|| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |\n 72|\n 73|Convergence = every expected node is `Applied`. Any node still pending when the\n## DEBUG-ROLLOUT.md\n 24|  known-good binary.\n*25|- A broker-touching candidate may be staged, but apply refuses it while\n*26|  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n 27|  first flow does not auto-cycle them.\n 28|\n 29|## One-time lab setup\n...\n 107|  current debug-channel version.\n*108|- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n 109|  resources; quiesce hosted endpoints first.\n 110|\n 111|## Agent checklist\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 15|| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n 16|| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n*17|| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n 18|| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19|| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20|| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n*21|| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n 22|| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-u...\n 23|| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner...\n*24|| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8...\n 25|| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruni...\n 26|| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27|| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n 28|| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\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...\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*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## DOCS-STRATEGY.md\n 123|`Messaging → Lifecycle → Terminal → Networking → Harness contract → Instances → Shells → Self-update`\n*124|(messaging is the primitive the quickstart needs; self-update last because its no-termination invariant depends on the terminal/broker model.)\n 125|**Per-vertical internal template (Django labels × Cloudflare ordering):**\n 126|`Overview (why+diagram) · Quickstart/Tutorial · How-to guides · Reference (rustdoc+CLI+schema) · llms.txt`\n 127|\n## DORMANCY-BUDGET.md\n 15|\n*16|1. binds a real (net-less) broker — the production PTY-hosting path — in a\n 17|   hermetic `SPT_HOME`;\n*18|2. spawns N idle seats through `Brain::spawn_session_pid` (one probe brain per\n 19|   seat, each labeled `budget-<i>`), exactly the shape of a **dormant** seat:\n*20|   PTY master + harness child + output ring, state-preserved and undriven;\n 21|3. settles, then samples each child's **RSS**, **handle/fd count**, and\n 22|   **cumulative CPU** twice across an idle window — the CPU delta is the idle\n 23|   burn of a warm seat;\n*24|4. samples the host process's RSS before/after spawning (the broker-side\n*25|   per-seat overhead: PTY plumbing + output rings);\n 26|5. kills every seat (the **suspended** state — only the on-disk perch record\n 27|   would remain in production) and samples the residual.\n 28|\n...\n 31|count, and `/proc/<pid>/stat` utime+stime. **Limitation:** the sample covers\n*32|the direct PTY child only — a harness that forks a worker tree under-reports\n 33|by the tree's cost (the claude numbers below are the main node process, which\n 34|dominates).\n 35|\n...\n 61|\n*62|Broker-side overhead: ~0.2–0.3 MiB per seat (RSS delta ÷ N). Suspended\n 63|residual: host process returns to its ~6–7 MiB baseline — a suspended seat\n 64|costs **only the on-disk perch record** (bytes, not megabytes, no handles, no\n 65|CPU).\n...\n 78|\n*79|Broker-side overhead: ~0.3 MiB per seat. Suspended residual: host process\n 80|back at its ~4.7 MiB baseline.\n 81|\n 82|## The policy (locked by these numbers)\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n*1|# F-027 — ENDPOINT_SPAWN_FAIL: pre-spawn seam + bind-deadline honesty (DESIGN)\n 2|\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 8|\n*9|Scope split: **Half A (bind-deadline honesty) and Half B (pre-spawn manifest\n 10|seam) are spt-core changes** (todlando's wave, sequenced after F-029). **The\n 11|trust-seeder binary itself is adapter-side** (perri's wave — spt-core stays\n 12|harness-agnostic per the glue-model boundary: adapters = manifest + binary only;\n...\n 18|\n*19|`spt endpoint run <id> --start` prints `ENDPOINT_RUN_STARTED` when the broker\n*20|has accepted the spawn RPC and a child PID exists — **nothing more is verified**\n 21|(cli.rs:1819 machine line, cli.rs:1833 `--start` success print). A harness that\n*22|launches but wedges post-spawn (workspace-trust prompt, null stdin) never fires\n 23|its SessionStart hook, so `establish_perch` never runs (api/startup.rs:299 is\n 24|the ONLY writer of the `ready` marker), and the endpoint sits `ready=false`\n 25|**forever, silently** — while the operator was told it started.\n...\n 28|cli.rs:2735/3826) but nothing gates or alarms on it: passive display only. The\n*29|attach path's `await_endpoint_session` (cli.rs:1854, 25 s) gates on broker\n*30|**session existence** — created synchronously at spawn, so it passes instantly\n 31|for a wedged harness — and the `--start` path skips even that.\n 32|\n*33|Precedent trap to avoid: `TRANSLATION_SPAWN_FAILED` (broker.rs:1271) is loud on\n 34|the **detached daemon's stderr, which nobody reads** (ADR-0022:130,\n 35|KNOWN-HAZARDS F-019 lesson). Any F-027 token must land on **CLI stderr and/or\n 36|the perch record**, never daemon-stderr-only.\n...\n 42|| 1 | TRUST-WEDGE — CC at workspace-trust dialog, null stdin; `ENDPOINT_RUN_STARTED` printed; ready=false forever | every fresh probe cwd, this session | SILENT |\n*43|| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n*44|| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n*45|| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n*46|| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n*47|| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n*48|| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n 49|\n 50|Seed-1 correction (accepted): the \"backslash-keyed `hasTrustDialogAccepted`\n 51|silently ignored\" pin from the RUNTRUTH triage was a **rig artifact** (hand-\n...\n 57|Seed-2 (the deeper root): trust is stored **per config root**, and the root a\n*58|spawn resolves is a **function of whether `CLAUDE_CONFIG_DIR` survives the\n*59|spawn env-rebuild** — which differs by spawn mode (detached daemon\n*60|`CreateEnvironmentBlock` rebuild vs foreground inherit). Two spawns on one box\n 61|resolved different accounts (Reavo vs Brandon). Seeding the operator shell's\n 62|root is therefore WRONG in general.\n 63|\n...\n 67|The silent class (modes 1, 2, 3, 6) must fail with the same loudness as the\n*68|pre-spawn class (modes 4, 5) already does.\n 69|\n 70|### Half A — bind-deadline honesty (spt-core, harness-agnostic)\n 71|\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 20|\n*21|- `livehost.rs:497 → host_one:518` spawns the psyche\n*22|  (`spawn_psyche_owned(Some(&session_id), None)`) and starts `run_pulse_loop`\n 23|  as its driver; `confirm_residency_or_unhost`\n 24|  (REQ-HAZARD-LIVEHOST-NONRESIDENT) probes the owned child and re-hosts every\n 25|  ~5 s reconcile tick when it isn't resident.\n...\n 27|  production callers**: `resume_psyche` (spt-live resume.rs:175, mode-validated\n*28|  spawn + nested rebind), `run_psyche_turn` (spt-live turn.rs:72, bounded\n 29|  stdin→stdout turn; its only production caller is the ADR-0013 conflict-merge\n 30|  in reconcile.rs), and `psyche_turn_and_relay` (spt-daemon psyrelay.rs:168,\n 31|  turn + intent-parse + sanitized relay).\n...\n 70|| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |\n*71|| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |\n 72|| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |\n 73|| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |\n 74|| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |\n...\n 80|\n*81|### W1 — wire the per-event driver (retire the resident spawn)\n 82|\n*83|`host_one` stops spawning a resident psyche. The pulse loop **stays as the\n 84|daemon-side scheduler** (its thread, stop-flag, and drop-dir watching are\n 85|correct); what changes is what a fire DOES: each pulse/event invokes **one\n 86|bounded per-event turn** through the existing stack —\n...\n 102|`psyche_host_error` (kept name; it is already the harness-reachable pattern) +\n*103|cooldown, reset on a successful turn. No respawn storm exists anymore because\n*104|there is nothing resident to respawn.\n 105|\n 106|### W2 — psyche-own sid custody\n 107|\n...\n 114|  psyche's conversational thread survives parent resets (that is its job).\n*115|- `resume_psyche(ResumeMode::…)` validates the custody key before spawn\n 116|  (already implemented — resume.rs:183).\n 117|- **Reseed path:** when the psyche's own session is lost/invalid (first host,\n 118|  store wipe, harness-side expiry), fall back to\n...\n 120|  role/live/project into the psyche-download file whose path fills\n*121|  `{psyche_context_file}` (file-always; non-empty content = fresh preload,\n 122|  0-byte = continue — replaces the former `{psyche_context}` body key) and a\n 123|  fresh psyche session seeds from it. Loud marker on reseed (`PSYCHE_RESEED:{id}`) so a\n 124|  custody-loss loop is visible, plus the W1 failure budget bounds it.\n...\n 160|\n*161|Remove the own-copy spawn path for psyche roles (spawn from the registry\n 162|install dir like every other bounded adapter invocation — image-lock exposure\n 163|is now turn-bounded, same class as `translate`/`digest` today). GC existing\n 164|`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n...\n 225|  0.13.2): psyche failure of ANY shape must not remove/alter the parent\n*226|  endpoint's ready/hosted state, and hosting must not churn-respawn.\n 227|  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n 228|  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229|- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n## F-035-CONFLICT.md\n 34|\n*35|For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n 36|idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37|\n 38|| carrier | `active_only` to spt-hosted? |\n## FAULT-MATRIX.md\n 3|**Why this exists.** ADR-0002 collapsed networking into the one per-machine daemon\n*4|(SERIOUS #6, Stage-A red team): a networking bug and a PTY bug now share a failure\n 5|domain. The collapse is locked — the owed mitigation is this matrix: inject a fault\n 6|into each subsystem **independently**, observe the blast radius (what else breaks)\n 7|and the recovery (what brings it back), and grow the proven rows until every\n...\n 19||---|---|---|---|---|---|\n*20|| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\n*21|| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\n*22|| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\n*23|| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\n*24|| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\n 25|| 6 | **Registry feed: stale/replayed update over the wire** | None — update dropped | Stored registry state (newer epoch holds) | None needed (epoch lease absorbs it) | `tests/replicate.rs::registries_converge_over_the_wire_and_the_lease_holds`; `replicate.rs` unit tests |\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*32|| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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...\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 48|| 28 | **Untrusted origin injects notif records** (unpaired peer, or a `home`-trusted peer spoofing into `work`) | None — dropped fail-closed at the apply gate, zero rows written; the gate's subject is the handshake-proven stream-table origin, never payload bytes (KH 7.5) | The spool (nothing materialized); every subnet the origin isn't trusted in (trust is per-subnet) | None needed; pair the node to entitle it | `tests/notifsync.rs` (gate-negative leg); `notifsync.rs::untrusted_and_cross_subnet_origin...\n 49|| 29 | **Notif record for a non-member subnet** (a peer feeds rows for a subnet this node never joined) | None — dropped fail-closed; the subnet is never materialized locally (the REQ-INST-13 posture: unconfigured replicates nowhere) | The member subnets' spools; the feed (other records still apply) | None needed | `notifsync.rs::non_member_subnet_record_never_materializes` |\n 50|\n...\n 54||---|---|---|---|---|\n*55|| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n*56|| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57|| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58|| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n 59|| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60|| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61|| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n## FLAKE-LEDGER.md\n 9||---|------|-------------|-----------|--------|\n*10|| 1 | `spt-daemon brain_restart_survives_gaplessly` | 1× hosted Windows (2026-06-03, hung) | ConPTY stall under hosted-runner load | job `timeout-minutes` bounds it; not seen self-hosted |\n*11|| 2 | `spt-term digest_parses_a_real_pty_stream` | 3× gravity (2026-06-03; 2026-06-04 runs 26957386850, 26966738231) | sprint-collapse assert (`digest.rs`) — PTY input echo and the child's write are **concurrent writers** into the master-read stream; back-to-back `send_line`s let a later line's echo interleave mid-`Write(…)`, breaking the tool regex for that turn | HARDENED (2026-06-04): sends serialized — each line pumped back (adaptive echo-copy count, 2s/line bound) before the next, so no concu...\n 12|| 3 | `spt-daemon sync.rs torn_pull` + `concurrent_writes` | 1× (2026-06-03) | `wait_for_stream` deadline too tight under load | HARDENED: `wait_for_stream_except` 2s→10s; no recurrence |","truncated":true,"truncatedBy":"bytes","totalLines":706,"totalBytes":94792,"outputLines":336,"outputBytes":50772,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":706,"totalBytes":94792,"outputLines":336,"outputBytes":50772,"shownRange":{"start":1,"end":336},"nextOffset":337}}},"isError":false,"timestamp":1784195738329}}
{"type":"message","id":"b23b9c74","parentId":"1f77e354","timestamp":"2026-07-16T09:55:51.537Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning targeted test module discovery**"},{"type":"toolCall","id":"call_fK3L0CSqHzwA9RccY96WUDJt|fc_0b42584a8ab0545d016a58aaa77ad08199a616c40090b39000","name":"read","arguments":{"i":"Reading domain ownership rules","path":"CONTEXT.md","selector":"1-220"}},{"type":"toolCall","id":"call_wyH7iSoeJUNnpGaaNgNJoH6n|fc_0b42584a8ab0545d016a58aaa77adc81998ae84a14aeb46f5e","name":"read","arguments":{"i":"Reading refresh E2E","path":"crates/spt/tests/daemon_refresh_e2e.rs","selector":"1-340"}},{"type":"toolCall","id":"call_64HdIMezdhpONggm79LdvYrp|fc_0b42584a8ab0545d016a58aaa77ae081999e21dfe9c977a3d1","name":"read","arguments":{"i":"Reading broker hazard history","path":"docs/KNOWN-HAZARDS.md","selector":"624-710"}},{"type":"toolCall","id":"call_eglr6knMPx6lkjbQLDLkkPLt|fc_0b42584a8ab0545d016a58aaa77af08199a74e1dbda258d449","name":"read","arguments":{"i":"Reading rollout safety rules","path":"docs/DEBUG-ROLLOUT.md","selector":"1-180"}},{"type":"toolCall","id":"call_ubMsJg2i8FQ0Tv44QjYcGH2k|fc_0b42584a8ab0545d016a58aaa77afc81998aae650dfcdfd8a7","name":"read","arguments":{"i":"Reading split decision","path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","selector":"1-240"}},{"type":"toolCall","id":"call_ZrTQBAu4DfmwUjdhf18vCtFV|fc_0b42584a8ab0545d016a58aaa77b0081998fb8fc6cbb454763","name":"grep","arguments":{"i":"Locating restart PTY tests","pattern":"brain_restart_survives|survives.*brain.*restart|brain.*restart.*gapless|attach_survives_target|controller.*restart|restart.*controller|multi.*controller|second.*controller","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_lRClXydTWMwv5QyVIDcyRMau|fc_0b42584a8ab0545d016a58aaa77b0881998758995056c8b3ac","name":"grep","arguments":{"i":"Locating process harness helpers","pattern":"struct Test|fn spawn|fn start|spawn_broker|start_broker|spawn_brain|start_brain|Command::new|controller","path":"crates/spt-daemon/tests;crates/spt/tests;crates/spt-broker/tests;crates/spt-broker/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_4z722OFSwCZVTc9OIyTGI1g7|fc_0b42584a8ab0545d016a58aaa77b1081998c3478a3aff6ad8b","name":"glob","arguments":{"i":"Mapping broker test suites","path":"crates/*/tests/*.rs;crates/*/src/tests/*.rs;crates/spt-broker/**/*test*.rs;crates/spt-daemon/**/*test*.rs","hidden":true,"gitignore":true,"limit":500}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":19977,"output":554,"cacheRead":13824,"cacheWrite":0,"totalTokens":34355,"reasoningTokens":104,"cost":{"input":0.099885,"output":0.01662,"cacheRead":0.006912,"cacheWrite":0,"total":0.123417}},"stopReason":"toolUse","timestamp":1784195738426,"responseId":"resp_0b42584a8ab0545d016a58aa9ad18c8199b6ef3195efcf2241","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aa9d24ac8199bfdbb0e815df95b5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqnKMR-bdee26cRPkopTcqrvo9UOImOK-fU29h22lEQ3ZG8ojzbX1CBPmAB53LAuuFHQ0axzwIOeV6FFCgR7YoKN6ngBrtCKXJuxOrFssqJqrJmHrkRHeHcVDk2sDYK1ZZwXluXftalKyeDQ25LQHsZKJbfnXy6Ih8XWPueYLL_DugTmmxFnVs0LQ2kJuarV0SGdD_trOQwbjp6sS3tBnXM9Ri47Us50hSoeTuiTp4Mm4VdMtNsh7E-qssocdddXovO1rg1Q4Hxwk3nK2hmr9WG7-0P7tYBh1pQb7KF1dxiJ37G77lO3WPgrkG15N6eVqU5NrHPSwWdXJ-gbLJvxbT98KZJt2o9n42D_JVWwYOIczDQ2a68QB6x0cPcZZmwJ-AzALPdyGBjB0ni7zOop78BVW5uKPyEs_jY6vRbiLyvQ1ah8x39CjXGW2AMUMwSlL63rKZ6sHnwSpHLuELzCEFAeA0XW5rfbQXJPLi6ETKptPQMx7Xu-hWyW4rLI2u2zHmnsgLSNAY7JLEAoU0LvA40XwrY1KIzyplNshT892NLeTSmDhTGoIJK9CGPqRYLZX5y-0qawPVhsv5RitmUHEB7wwllKigsaJaBCcnqhqTVS8FLv5knTqPG3ZoaoOz1qWR5blU2K13fbjyalykckcajlcl1vTCKbJZf0-eD9OpkxUU1WW9l1f1hqthp31RW3i7BweMf8Sqna68xDLJC_HkxxFZkS_PotdzIC4DOPK-4-HosB2W48VPg5R0MH7gRghpIANadZVHvZGFzdSYFJBu8fBbLqNudUuW0h4JIKHXQD2K3rr1QTIEX2jdO2XvlCPCs4Nrs9_BHkijNsC8X5Xxu_xDTxApGPfgwyCQCM9CKn6PnDZ1Kfkib0ZkLvTJKgpxRmQ3kBHxZ2GN2kjOsWEdatQDUV7aNV3vJeioLLxL6KzNWEpoM8ROx3frY14IcoQodLQpu_qfrkGl1xUWiMtpYsizNOzgoSJ-DaPzaU3xzM80kmjfPCxc7Bo0Ho_8i6iC_g6gp_IMLKlL0oA0Qh4dzoR91gTE8_OcphtIOr_c4BKYBWzgEGPv0BCSbnusw3wGKwLIZQPprUILM6mpgFEVaAgGnpO8pX5V6dU__I702bNExqqkC9qD72br-koboeCvl928JA-dnuNhT1lbBadQrqvUBTWVbpzDIhXxZMXrt0Nddtm35mMz6WLO05n-u7pDgH4v4huxo5MzoKzuspkg8eLtx6pA69UZCa_HhrnIDnSBEyFQYa2Z63E7yoU-ee8bvDYjTqHaSr4pm6pfhyDKgHGtbAjgUhaclE14zVR7HlMCENJdtAXSOhseGaW1Bqe30_RFyaoYgYMVTrgAZ8UFK0Ud2-VGFn95W8uBg1fTkKezZZHfRyVaLcfT_SoCsY86rL8j5M8aUoXcYJp5WQkLKlYly2RGCTIRPpj84-O9RfX0_6jrMoYBLKLHbe3amLahxYiVwVBOCTD1HwqGxJ3hqX7upxbRg-Mq6qhTSe25WI_QpzDc2JUOH3j3YXSn4CdKnI_FbZc4fV_4v-SeNb_DPaYhirw51KAWyOrpvAFzXtrRhebsOkTKsu88W3wcrkX4T","summary":[{"type":"summary_text","text":"**Planning targeted test module discovery**"}]},{"id":"fc_0b42584a8ab0545d016a58aaa77ad08199a616c40090b39000","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading domain ownership rules\",\"path\":\"CONTEXT.md\",\"selector\":\"1-220\"}","call_id":"call_fK3L0CSqHzwA9RccY96WUDJt","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaa77adc81998ae84a14aeb46f5e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh E2E\",\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"selector\":\"1-340\"}","call_id":"call_wyH7iSoeJUNnpGaaNgNJoH6n","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaa77ae081999e21dfe9c977a3d1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker hazard history\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"624-710\"}","call_id":"call_64HdIMezdhpONggm79LdvYrp","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaa77af08199a74e1dbda258d449","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rollout safety rules\",\"path\":\"docs/DEBUG-ROLLOUT.md\",\"selector\":\"1-180\"}","call_id":"call_eglr6knMPx6lkjbQLDLkkPLt","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaa77afc81998aae650dfcdfd8a7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading split decision\",\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"selector\":\"1-240\"}","call_id":"call_ubMsJg2i8FQ0Tv44QjYcGH2k","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaa77b0081998fb8fc6cbb454763","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating restart PTY tests\",\"pattern\":\"brain_restart_survives|survives.*brain.*restart|brain.*restart.*gapless|attach_survives_target|controller.*restart|restart.*controller|multi.*controller|second.*controller\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_ZrTQBAu4DfmwUjdhf18vCtFV","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aaa77b0881998758995056c8b3ac","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating process harness helpers\",\"pattern\":\"struct Test|fn spawn|fn start|spawn_broker|start_broker|spawn_brain|start_brain|Command::new|controller\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests;crates/spt-broker/tests;crates/spt-broker/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_lRClXydTWMwv5QyVIDcyRMau","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aaa77b1081998c3478a3aff6ad8b","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping broker test suites\",\"path\":\"crates/*/tests/*.rs;crates/*/src/tests/*.rs;crates/spt-broker/**/*test*.rs;crates/spt-daemon/**/*test*.rs\",\"hidden\":true,\"gitignore\":true,\"limit\":500}","call_id":"call_4z722OFSwCZVTc9OIyTGI1g7","name":"glob"}]},"duration":13109.860700000077,"ttft":2693.0082000000402,"contextSnapshot":{"promptTokens":33801,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fK3L0CSqHzwA9RccY96WUDJt|fc_0b42584a8ab0545d016a58aaa77ad08199a616c40090b39000","toolName":"read","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"CONTEXT.md"},"intent":"Reading domain ownership rules"},"id":"118ae146","parentId":"b23b9c74","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wyH7iSoeJUNnpGaaNgNJoH6n|fc_0b42584a8ab0545d016a58aaa77adc81998ae84a14aeb46f5e","toolName":"read","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"crates/spt/tests/daemon_refresh_e2e.rs"},"intent":"Reading refresh E2E"},"id":"4c831e2f","parentId":"118ae146","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_64HdIMezdhpONggm79LdvYrp|fc_0b42584a8ab0545d016a58aaa77ae081999e21dfe9c977a3d1","toolName":"read","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading broker hazard history"},"id":"60129791","parentId":"4c831e2f","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eglr6knMPx6lkjbQLDLkkPLt|fc_0b42584a8ab0545d016a58aaa77af08199a74e1dbda258d449","toolName":"read","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"docs/DEBUG-ROLLOUT.md"},"intent":"Reading rollout safety rules"},"id":"73bdd0b9","parentId":"60129791","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ubMsJg2i8FQ0Tv44QjYcGH2k|fc_0b42584a8ab0545d016a58aaa77afc81998aae650dfcdfd8a7","toolName":"read","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md"},"intent":"Reading split decision"},"id":"f3063101","parentId":"73bdd0b9","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZrTQBAu4DfmwUjdhf18vCtFV|fc_0b42584a8ab0545d016a58aaa77b0081998fb8fc6cbb454763","toolName":"grep","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"crates"},"intent":"Locating restart PTY tests"},"id":"2caf3be5","parentId":"f3063101","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lRClXydTWMwv5QyVIDcyRMau|fc_0b42584a8ab0545d016a58aaa77b0881998758995056c8b3ac","toolName":"grep","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests;crates/spt-broker/tests;crates/spt-broker/src"},"intent":"Locating process harness helpers"},"id":"1d4a0a8c","parentId":"2caf3be5","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4z722OFSwCZVTc9OIyTGI1g7|fc_0b42584a8ab0545d016a58aaa77b1081998c3478a3aff6ad8b","toolName":"glob","startedAt":"2026-07-16T09:55:51.538Z","args":{"path":"crates/*/tests/*.rs;crates/*/src/tests/*.rs;crates/spt-broker/**/*test*.rs;crates/spt-daemon/**/*test*.rs"},"intent":"Mapping broker test suites"},"id":"72eed9a5","parentId":"1d4a0a8c","timestamp":"2026-07-16T09:55:51.538Z"}
{"type":"message","id":"03fa0972","parentId":"72eed9a5","timestamp":"2026-07-16T09:55:51.542Z","message":{"role":"toolResult","toolCallId":"call_eglr6knMPx6lkjbQLDLkkPLt|fc_0b42584a8ab0545d016a58aaa77af08199a74e1dbda258d449","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":1784195751541}}
{"type":"message","id":"81ba1f58","parentId":"03fa0972","timestamp":"2026-07-16T09:55:51.542Z","message":{"role":"toolResult","toolCallId":"call_wyH7iSoeJUNnpGaaNgNJoH6n|fc_0b42584a8ab0545d016a58aaa77adc81998ae84a14aeb46f5e","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":1784195751541}}
{"type":"message","id":"54fc879c","parentId":"81ba1f58","timestamp":"2026-07-16T09:55:51.542Z","message":{"role":"toolResult","toolCallId":"call_ubMsJg2i8FQ0Tv44QjYcGH2k|fc_0b42584a8ab0545d016a58aaa77afc81998aae650dfcdfd8a7","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":1784195751542}}
{"type":"message","id":"02d9d079","parentId":"54fc879c","timestamp":"2026-07-16T09:55:51.567Z","message":{"role":"toolResult","toolCallId":"call_4z722OFSwCZVTc9OIyTGI1g7|fc_0b42584a8ab0545d016a58aaa77b1081998c3478a3aff6ad8b","toolName":"glob","content":[{"type":"text","text":"# crates/\n## spt/tests/\nprojindex_reader_e2e.rs\ndummy_harness_e2e.rs\nprojindex_writer_e2e.rs\njson_emit.rs\nwhoami_identity_e2e.rs\ndaemon_refresh_e2e.rs\ncomposite_e2e.rs\ndocs_bundle_e2e.rs\nrelease_verify_e2e.rs\nbootstrap_verb_e2e.rs\ntrial_drain_drive_e2e.rs\nresume_no_control_steal_e2e.rs\nsend_stamp_agent_id_e2e.rs\nendpoint_autostart_e2e.rs\nlist_json_liveness_parity_e2e.rs\nlisten_seed_retry_e2e.rs\nactive_only_never_relay_e2e.rs\nn1_pairing.rs\nattach_wedge_e2e.rs\nworker_visibility_e2e.rs\nworker_lifecycle_e2e.rs\ntwohost_cli.rs\ntranslate_proof.rs\npsyche_download_e2e.rs\nnotify_shell_e2e.rs\nmulti_subnet_bringup_e2e.rs\nlive_adapt_translation_swap_e2e.rs\nbrain_survive.rs\ntunnel_e2e.rs\nshell_sleepwake_e2e.rs\nshell_e2e.rs\nshell_actgate_e2e.rs\nrun_no_dup_session_e2e.rs\nresume_template_e2e.rs\nready_resume_ledger_e2e.rs\nquickstart_e2e.rs\npsyche_sid_custody_e2e.rs\npoll_envelope_e2e.rs\noneliner_e2e.rs\nnested_resolution_e2e.rs\nn1_compat.rs\nlive_resolve_e2e.rs\nlive_firsthost_e2e.rs\nlive_bind_firsthost_e2e.rs\nlivehost_bootgate_e2e.rs\njob_escape_e2e.rs\nidle_edge_drain_e2e.rs\ngateway_owner_shell_e2e.rs\ngateway_e2e.rs\ndrive_e2e.rs\ncontract_e2e.rs\nbrain_split.rs\nbrain_respawn_rename.rs\nboundary_ready_strand_e2e.rs\nbind_honest_cross_perch_e2e.rs\nbind_cwd_project_e2e.rs\nbind_adapter_profile_persist_e2e.rs\nadapter_post_step.rs\n## spt-daemon/\n### tests/\ndocs_server_e2e.rs\npropagate.rs\npumpdeadline.rs\npump.rs\nfalse_promote.rs\nnet_worker_starve.rs\nbrain_resume_conn_deadlock.rs\nconn_blackhole_lifecycle.rs\nbrain_decouple.rs\ninject_control_wedge.rs\ntwo_origin_spanning.rs\nwake_single_flight.rs\nendpoint_survival.rs\nxfer.rs\nwanmsg.rs\ntwohost.rs\nreplicate.rs\nsync.rs\nnotifsync.rs\nnetstream.rs\nnetbroker.rs\ninput_ack_deadlock.rs\nidempotent.rs\ndispatch.rs\ndaemon_e2e.rs\nbroker.rs\nattach.rs\naccess.rs\nshellchan.rs\nseedproofx.rs\nrosterprop.rs\nresume.rs\nreseed.rs\npsyche_residency_expectation_e2e.rs\npsyche_event_turn_e2e.rs\npsyche_context_file_e2e.rs\npresence.rs\npairjoin.rs\nmesh.rs\nlegacy_resident_sweep_e2e.rs\nhandoff.rs\ndriven_by_selfheal.rs\ndigest.rs\ndaemon_lifecycle_real_brain.rs\ncontrol_stamp_lifetime.rs\nbudget.rs\nbrain_swap.rs\n### src/\ntest_home.rs\n## spt-store/tests/\ncarrier_claim_int.rs\n## spt-term/tests/\nwinspawn.rs\nsurface.rs\nstream.rs\ninject.rs\ndsr.rs\n## spt-msg/tests/\nkiller_quickstart.rs\n\nSkipped missing paths: crates/spt-broker/**/*test*.rs"}],"details":{"scopePath":"crates/*/tests/*.rs, crates/*/src/tests/*.rs, crates/spt-daemon/**/*test*.rs","fileCount":113,"files":["crates/spt/tests/projindex_reader_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/tests/projindex_writer_e2e.rs","crates/spt/tests/json_emit.rs","crates/spt/tests/whoami_identity_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/composite_e2e.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt-daemon/tests/propagate.rs","crates/spt/tests/release_verify_e2e.rs","crates/spt/tests/bootstrap_verb_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/send_stamp_agent_id_e2e.rs","crates/spt/tests/endpoint_autostart_e2e.rs","crates/spt/tests/list_json_liveness_parity_e2e.rs","crates/spt/tests/listen_seed_retry_e2e.rs","crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt-store/tests/carrier_claim_int.rs","crates/spt-daemon/tests/pumpdeadline.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/net_worker_starve.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt/tests/n1_pairing.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt-daemon/tests/two_origin_spanning.rs","crates/spt-daemon/tests/wake_single_flight.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt/tests/worker_visibility_e2e.rs","crates/spt/tests/worker_lifecycle_e2e.rs","crates/spt/tests/twohost_cli.rs","crates/spt/tests/translate_proof.rs","crates/spt/tests/psyche_download_e2e.rs","crates/spt/tests/notify_shell_e2e.rs","crates/spt/tests/multi_subnet_bringup_e2e.rs","crates/spt/tests/live_adapt_translation_swap_e2e.rs","crates/spt/tests/brain_survive.rs","crates/spt-daemon/tests/xfer.rs","crates/spt-daemon/tests/wanmsg.rs","crates/spt-daemon/tests/twohost.rs","crates/spt-daemon/tests/replicate.rs","crates/spt-daemon/tests/sync.rs","crates/spt-daemon/tests/notifsync.rs","crates/spt-daemon/tests/netstream.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/idempotent.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/access.rs","crates/spt-term/tests/winspawn.rs","crates/spt-term/tests/surface.rs","crates/spt-term/tests/stream.rs","crates/spt-term/tests/inject.rs","crates/spt-term/tests/dsr.rs","crates/spt-msg/tests/killer_quickstart.rs","crates/spt-daemon/tests/shellchan.rs","crates/spt-daemon/tests/seedproofx.rs","crates/spt-daemon/tests/rosterprop.rs","crates/spt-daemon/tests/resume.rs","crates/spt-daemon/tests/reseed.rs","crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","crates/spt-daemon/tests/psyche_event_turn_e2e.rs","crates/spt-daemon/tests/psyche_context_file_e2e.rs","crates/spt-daemon/tests/presence.rs","crates/spt-daemon/tests/pairjoin.rs","crates/spt-daemon/tests/mesh.rs","crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/driven_by_selfheal.rs","crates/spt-daemon/tests/digest.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/budget.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-daemon/src/test_home.rs","crates/spt/tests/tunnel_e2e.rs","crates/spt/tests/shell_sleepwake_e2e.rs","crates/spt/tests/shell_e2e.rs","crates/spt/tests/shell_actgate_e2e.rs","crates/spt/tests/run_no_dup_session_e2e.rs","crates/spt/tests/resume_template_e2e.rs","crates/spt/tests/ready_resume_ledger_e2e.rs","crates/spt/tests/quickstart_e2e.rs","crates/spt/tests/psyche_sid_custody_e2e.rs","crates/spt/tests/poll_envelope_e2e.rs","crates/spt/tests/oneliner_e2e.rs","crates/spt/tests/nested_resolution_e2e.rs","crates/spt/tests/n1_compat.rs","crates/spt/tests/live_resolve_e2e.rs","crates/spt/tests/live_firsthost_e2e.rs","crates/spt/tests/live_bind_firsthost_e2e.rs","crates/spt/tests/livehost_bootgate_e2e.rs","crates/spt/tests/job_escape_e2e.rs","crates/spt/tests/idle_edge_drain_e2e.rs","crates/spt/tests/gateway_owner_shell_e2e.rs","crates/spt/tests/gateway_e2e.rs","crates/spt/tests/drive_e2e.rs","crates/spt/tests/contract_e2e.rs","crates/spt/tests/brain_split.rs","crates/spt/tests/brain_respawn_rename.rs","crates/spt/tests/boundary_ready_strand_e2e.rs","crates/spt/tests/bind_honest_cross_perch_e2e.rs","crates/spt/tests/bind_cwd_project_e2e.rs","crates/spt/tests/bind_adapter_profile_persist_e2e.rs","crates/spt/tests/adapter_post_step.rs"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","missingPaths":["crates/spt-broker/**/*test*.rs"]},"isError":false,"timestamp":1784195751567}}
{"type":"message","id":"9b99e771","parentId":"02d9d079","timestamp":"2026-07-16T09:55:51.568Z","message":{"role":"toolResult","toolCallId":"call_ZrTQBAu4DfmwUjdhf18vCtFV|fc_0b42584a8ab0545d016a58aaa77b0081998fb8fc6cbb454763","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### attach.rs\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### brain.rs\n 1126|    /// Query the RUNNING broker's own compiled image version (F-025). The broker\n*1127|    /// survives `spt update apply` (only the brain restarts — ADR-0018 D3-3), so\n 1128|    /// this is the authoritative \"what version is actually executing\" — read from\n 1129|    /// the broker's compile-time constant, NEVER inferred from disk. Returns\n 1130|    /// `None` when the broker is too old to answer: an older broker replies\n### broker.rs\n 5505|    ///\n*5506|    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n 5507|    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\n 5508|    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\n 5509|    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\n### msg.rs\n 92|/// Brain→broker: report the broker's OWN compiled image version (F-025). The\n*93|/// broker SURVIVES `spt update apply` (only the brain restarts — ADR-0018 D3-3),\n 94|/// so a freshly-applied node may still be RUNNING an older broker binary; the\n 95|/// broker is the authoritative source for \"what version is actually executing\",\n 96|/// answered from its own compile-time constant — NEVER inferred from disk. An\n### nethost.rs\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 303|    next_seq: u64,\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 1110|// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n*1111|// a successor re-taking the controller slot after a brain restart — silently\n 1112|// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n 1113|// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n 1114|// no Displaced reaches the first connection.\n...\n 1309|// [int->REQ-RCVIEW-1] GATE #7 COMBINED (doyle's named close-out): a controller +\n*1310|// a viewer are both attached when the controller's serving brain RESTARTS (its\n 1311|// connection drops; a successor re-serves with the SAME operator identity). The\n 1312|// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\n 1313|// the VIEWER survives the churn and keeps receiving output. This is the self-kick\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 1341|    // re-take. It silently becomes controller (NEVER displacing anyone): the\n*1342|    // gate-#7 self-kick guard on a real controller-conn restart.\n 1343|    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\n 1344|    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"succ control\");\n 1345|    assert_eq!(\n...\n 1350|\n*1351|    // The viewer SURVIVED the controller restart: output driven by the successor\n 1352|    // reaches it, and it NEVER saw a Displaced.\n 1353|    succ.send_effect(MintedOp::new(Minter::Rc, 2), b\"AFTER\\r\").expect(\"after marker\");\n 1354|    assert!(\n 1355|        viewer_reads_marker_no_displace(&mut view, b\"AFTER\"),\n*1356|        \"viewer survives the controller restart and keeps receiving (no Displaced)\"\n 1357|    );\n 1358|}\n 1359|\n...\n 1374|// that the epoch gate eliminates the two-writer race lives in the broker int\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### broker.rs\n 992|// epoch gate) stops a SUPERSEDED writer that has not yet committed, but a frame\n*993|// the stale writer-A committed BEFORE the second `become_controller` bumps the\n 994|// epoch is irreducible at the broker (per the controller_writer doc-comment). Fix\n 995|// #1 removes the contradictory double-take in production; fix #3 makes the\n 996|// consumer dedup-below+snap-above tolerant. The INVARIANT a consumer must always\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...\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### 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### 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### xfer.rs\n 232|#[test]\n*233|fn push_survives_target_brain_restart_exactly_once() {\n 234|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 235|    let (name_a, name_b) = (unique_name(), unique_name());\n 236|    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":21,"fileCount":12,"files":["crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/netstream.rs","crates/spt-daemon/tests/presence.rs","crates/spt-daemon/tests/xfer.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/attach.rs","count":1},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":1},{"path":"crates/spt-daemon/src/msg.rs","count":1},{"path":"crates/spt-daemon/src/nethost.rs","count":1},{"path":"crates/spt-daemon/tests/attach.rs","count":9},{"path":"crates/spt-daemon/tests/broker.rs","count":1},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":2},{"path":"crates/spt-daemon/tests/handoff.rs","count":1},{"path":"crates/spt-daemon/tests/netstream.rs","count":1},{"path":"crates/spt-daemon/tests/presence.rs","count":1},{"path":"crates/spt-daemon/tests/xfer.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## src/\n### attach.rs\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### brain.rs\n 1126│    /// Query the RUNNING broker's own compiled image version (F-025). The broker\n*1127│    /// survives `spt update apply` (only the brain restarts — ADR-0018 D3-3), so\n 1128│    /// this is the authoritative \"what version is actually executing\" — read from\n 1129│    /// the broker's compile-time constant, NEVER inferred from disk. Returns\n 1130│    /// `None` when the broker is too old to answer: an older broker replies\n### broker.rs\n 5505│    ///\n*5506│    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n 5507│    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\n 5508│    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\n 5509│    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\n### msg.rs\n 92│/// Brain→broker: report the broker's OWN compiled image version (F-025). The\n*93│/// broker SURVIVES `spt update apply` (only the brain restarts — ADR-0018 D3-3),\n 94│/// so a freshly-applied node may still be RUNNING an older broker binary; the\n 95│/// broker is the authoritative source for \"what version is actually executing\",\n 96│/// answered from its own compile-time constant — NEVER inferred from disk. An\n### nethost.rs\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 303│    next_seq: u64,\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 1110│// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n*1111│// a successor re-taking the controller slot after a brain restart — silently\n 1112│// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n 1113│// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n 1114│// no Displaced reaches the first connection.\n     │...\n 1309│// [int->REQ-RCVIEW-1] GATE #7 COMBINED (doyle's named close-out): a controller +\n*1310│// a viewer are both attached when the controller's serving brain RESTARTS (its\n 1311│// connection drops; a successor re-serves with the SAME operator identity). The\n 1312│// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\n 1313│// the VIEWER survives the churn and keeps receiving output. This is the self-kick\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 1341│    // re-take. It silently becomes controller (NEVER displacing anyone): the\n*1342│    // gate-#7 self-kick guard on a real controller-conn restart.\n 1343│    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\n 1344│    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"succ control\");\n 1345│    assert_eq!(\n     │...\n 1350│\n*1351│    // The viewer SURVIVED the controller restart: output driven by the successor\n 1352│    // reaches it, and it NEVER saw a Displaced.\n 1353│    succ.send_effect(MintedOp::new(Minter::Rc, 2), b\"AFTER\\r\").expect(\"after marker\");\n 1354│    assert!(\n 1355│        viewer_reads_marker_no_displace(&mut view, b\"AFTER\"),\n*1356│        \"viewer survives the controller restart and keeps receiving (no Displaced)\"\n 1357│    );\n 1358│}\n 1359│\n     │...\n 1374│// that the epoch gate eliminates the two-writer race lives in the broker int\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### broker.rs\n 992│// epoch gate) stops a SUPERSEDED writer that has not yet committed, but a frame\n*993│// the stale writer-A committed BEFORE the second `become_controller` bumps the\n 994│// epoch is irreducible at the broker (per the controller_writer doc-comment). Fix\n 995│// #1 removes the contradictory double-take in production; fix #3 makes the\n 996│// consumer dedup-below+snap-above tolerant. The INVARIANT a consumer must always\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    │...\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### 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### 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### xfer.rs\n 232│#[test]\n*233│fn push_survives_target_brain_restart_exactly_once() {\n 234│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 235│    let (name_a, name_b) = (unique_name(), unique_name());\n 236│    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));"},"isError":false,"timestamp":1784195751568}}
{"type":"message","id":"7678b4d8","parentId":"9b99e771","timestamp":"2026-07-16T09:55:51.587Z","message":{"role":"toolResult","toolCallId":"call_lRClXydTWMwv5QyVIDcyRMau|fc_0b42584a8ab0545d016a58aaa77b0881998758995056c8b3ac","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...\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 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 1015|    // The clear is now BROKER-owned (REQ-RCVIEW-1, single writer): the worker's\n*1016|    // detach releases the controller slot over IPC, and the broker re-stamps\n 1017|    // driven_by=None asynchronously — so poll for it (production clears the same\n 1018|    // async way when the worker connection drops).\n 1019|    let cleared = (0..200).any(|_| {\n...\n 1032|\n*1033|// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n*1034|// These exercise the OutputLog controller/viewer fan-out directly over the local\n 1035|// broker IPC: each `attach_as` carries a distinct `by` identity, so the role\n 1036|// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n 1037|// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n...\n 1041|/// Spawn a LABELED echo session on a fresh broker; return (broker, name, brain,\n*1042|/// sid). The spawner is the broker's by=None local controller placeholder.\n 1043|fn labeled_session(label: &str) -> (Arc<Broker>, String, Brain, u64) {\n 1044|    init_home();\n 1045|    let dir = HOME.get().expect(\"home\").path().to_path_buf();\n...\n 1069|\n*1070|// [int->REQ-RCVIEW-1] [int->REQ-KICK-1] The controller/viewer/take matrix and the\n 1071|// loud kick over the broker fan-out: a FREE endpoint's first Control becomes\n*1072|// controller; a DIFFERENT operator's Control is BUSY-REFUSED (never a silent\n 1073|// displace, gate #4); a Viewer coexists; an explicit Take displaces the\n*1074|// incumbent with a LOUD `Displaced{by}` and becomes controller.\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 1080|    let mut a = connect_retry(&name);\n 1081|    a.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"A control\");\n*1082|    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, \"free → controller\");\n 1083|\n 1084|    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a\n 1085|    // silent displace (gate #4). A is untouched.\n...\n 1090|        SubscribeOutcome::BusyControlled { by: \"node-A\".to_string() },\n*1091|        \"controlled → busy-refuse with the controller's identity\"\n 1092|    );\n 1093|\n 1094|    // A viewer coexists (read-only; never displaces).\n...\n 1099|    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes\n*1100|    // controller. This is the ONLY displace path.\n 1101|    b.attach_as(sid, 0, AttachIntent::Take, Some(\"node-B\")).expect(\"B take\");\n 1102|    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, \"take → took control\");\n 1103|    let displaced = (0..2000).find_map(|_| match a.read_event().expect(\"A event\") {\n...\n 1110|// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n*1111|// a successor re-taking the controller slot after a brain restart — silently\n 1112|// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n 1113|// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n 1114|// no Displaced reaches the first connection.\n...\n 1133|        SubscribeOutcome::Controller,\n*1134|        \"same-origin re-subscribe silently re-takes the controller\"\n 1135|    );\n 1136|\n 1137|    // The first connection must NOT receive a Displaced — re-taking your own slot\n...\n 1157|// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a\n*1158|// viewer's connection is refused by the broker (gate #3); the controller's\n 1159|// resize is honored.\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*1165|    // controller's resize is honored ⇒ NO Error frame arrives), and the\n 1166|    // `read_event_until` deadline is only honored on the pump carrier — a Whole\n 1167|    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`\n 1168|    // emits nothing on resize, unlike Windows `findstr`).\n### brain_decouple.rs\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...\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...\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 85|    #[cfg(windows)]\n*86|    let _ = std::process::Command::new(\"taskkill\")\n 87|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 88|        .output();\n 89|    #[cfg(unix)]\n*90|    let _ = std::process::Command::new(\"kill\")\n 91|        .args([\"-9\", &pid.to_string()])\n 92|        .output();\n 93|}\n...\n 95|/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output case\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...\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 237|    // ── Spawner A: spawn the flood child (dispatch_spawn pre-attaches A as the\n*238|    //    LOCAL controller, by=None). ──\n 239|    let mut a = connect(&name);\n 240|    send(\n 241|        &mut a,\n...\n 251|\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 257|    //    isolates the fix from the pre-existing \"a remote takes an UNDRIVEN\n...\n 287|    });\n*288|    // Give R time to become the controller and its writer to wedge on the flood.\n 289|    thread::sleep(Duration::from_millis(300));\n*290|    // The local spawner is displaced by R's take; drop it (no longer the controller).\n 291|    drop(a);\n 292|\n 293|    // ── Viewer V: a SEPARATE conn that DOES drain. Prove the fan-out keeps ticking\n...\n 347|\n*348|    // The viewer kept ticking while the controller conn was wedged.\n 349|    assert!(\n 350|        ticks_during_wedge > 0,\n*351|        \"an attached viewer must keep receiving output while the controller conn is \\\n 352|         wedged — the fan-out must never block on the stalled brain conn\"\n 353|    );\n 354|    // The take COMPLETED and took control (the wedged incumbent was stall-evicted).\n...\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 382|// completes. But hertz's RCA (`docs/UPDATE-WEDGE-ROOT-CAUSE.md` @ f8596ca,\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### brain_resume_conn_deadlock.rs\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 65|    #[cfg(windows)]\n*66|    let _ = std::process::Command::new(\"taskkill\")\n 67|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 68|        .output();\n 69|    #[cfg(unix)]\n*70|    let _ = std::process::Command::new(\"kill\")\n 71|        .args([\"-9\", &pid.to_string()])\n 72|        .output();\n 73|}\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 118|    let mut c = connect(name);\n 119|    write_frame(\n 120|        &mut c,\n...\n 195|        pids.push(pid);\n*196|        conns.push(conn); // hold the spawner controller conns open (undrained)\n 197|    }\n 198|    // Let the floods get going so the brain conn genuinely backs up on subscribe.\n 199|    thread::sleep(Duration::from_millis(600));\n...\n 302|    // this STRUCTURAL check — we reuse spawn_flood only to host real sessions and hold\n*303|    // the spawner controller conns + pids open, exactly like heartbeat_ticks_under_flood.\n 304|    let mut conns = Vec::new();\n 305|    let mut pids = Vec::new();\n 306|    let mut sids = Vec::new();\n...\n 310|        pids.push(pid);\n*311|        conns.push(conn); // hold the spawner controller conns open (undrained)\n 312|    }\n 313|\n 314|    // POSITIVE arm — the SHIPPING daemon resume path: cursor-only, NO subscribe.\n### broker.rs\n 421|#[test]\n*422|fn spawn_env_reaches_child() {\n 423|    env_inject_test();\n 424|}\n 425|\n...\n 709|/// interleaved early output into `out`).\n*710|fn spawn_echo(conn: &mut Stream, out: &mut Vec<u8>) -> u64 {\n 711|    send(conn, KIND_SPAWN, serde_json::to_value(echo_spawn_req()).unwrap());\n 712|    loop {\n 713|        let f = read_frame(conn).expect(\"frame before spawned\");\n...\n 982|// ── v0.13.0 P1c — REQ-HAZARD-CONTROLLER-WRITER-REORDER ────────────────────────\n*983|// THE INT KEYSTONE: deterministically drive the two-`become_controller`-on-ONE-\n 984|// connection ordering through the PUBLIC broker path (a brain that subscribes\n 985|// from_seq=1, then re-subscribes from_seq=0 over the SAME conn — reproducing the\n 986|// pre-fix handoff(eager subscribe @ prior.next_seq=1) + re-serve(attach_as(sid,0))\n...\n 992|// epoch gate) stops a SUPERSEDED writer that has not yet committed, but a frame\n*993|// the stale writer-A committed BEFORE the second `become_controller` bumps the\n*994|// epoch is irreducible at the broker (per the controller_writer doc-comment). Fix\n 995|// #1 removes the contradictory double-take in production; fix #3 makes the\n 996|// consumer dedup-below+snap-above tolerant. The INVARIANT a consumer must always\n 997|// see is therefore: the ACCEPTED seq stream is monotonic non-decreasing and\n...\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 1023|\n*1024|    // Spawn the echo child; the broker auto-subscribes this conn as controller.\n 1025|    send(\n 1026|        &mut conn,\n 1027|        KIND_SPAWN,\n...\n 1054|                self.accepted_seqs.last().is_none_or(|&last| seq > last),\n*1055|                \"accepted controller seq stream must be strictly forward (monotonic, \\\n 1056|                 exactly-once): seq {seq} arrived after {:?} — a reorder would \\\n 1057|                 surface here as the `got seq 1 want 0` gap\",\n 1058|                self.accepted_seqs\n...\n 1109|\n*1110|    // THE RACE: two `become_controller` on this ONE connection, back-to-back —\n*1111|    //   (1) subscribe from_seq=1  → broker become_controller(from_seq=1), initial=[1]\n 1112|    //       spawns writer-A (the pre-fix eager-handoff registration).\n*1113|    //   (2) subscribe from_seq=0  → broker become_controller(from_seq=0), initial=[0..]\n*1114|    //       bumps controller_epoch, spawns writer-B (the re-serve attach_as(sid,0)).\n 1115|    // Both `by:None`+Control = SAME identity → each is a silent re-take (gate #7),\n 1116|    // exactly the brain-restart re-serve shape. Pre-fix writer-A's stale seq 1 can\n 1117|    // win the socket ahead of writer-B's seq 0 → reorder.\n...\n 1133|    // ACCEPTED both bounds the read AND proves the session stayed LIVE through the\n*1134|    // double controller swap (input still reaches the PTY, output still flows).\n 1135|    send(\n 1136|        &mut conn,\n 1137|        KIND_INPUT,\n...\n 1157|            std::time::Instant::now() < deadline,\n*1158|            \"liveness marker P1C_LIVE must arrive through the re-served controller \\\n 1159|             within the watchdog — a serve thread that died on a gap (the reorder \\\n 1160|             HANG face) never re-pumps it. accepted={:?}\",\n 1161|            dedup.accepted_seqs\n...\n 1175|        dedup.accepted_seqs.windows(2).all(|w| w[0] < w[1]),\n*1176|        \"the accepted controller seq stream is strictly increasing end-to-end \\\n*1177|         (monotonic + exactly-once across the double become_controller): {:?}\",\n 1178|        dedup.accepted_seqs\n 1179|    );\n 1180|    let mut full = ground.clone();\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### budget.rs\n 219|fn sample_pid(pid: u32) -> Option<Sample> {\n*220|    let out = std::process::Command::new(\"powershell\")\n 221|        .args([\n 222|            \"-NoProfile\",\n 223|            \"-Command\",\n### conn_blackhole_lifecycle.rs\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 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 118|}\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 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 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 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 576|    //    ADVANCED across the whole incident window (baseline → now). It kept\n*577|    //    receiving output while B's controller conn was black-holed + retired. ──\n 578|    let u_final = u_frames.load(Ordering::Relaxed);\n 579|\n 580|    // ── Teardown BEFORE asserting (so a failing assert leaves no orphan child):\n...\n 606|        old_client_write_is_err.is_some(),\n*607|        \"HARNESS: the black-holed controller never handed its stream back — the \\\n 608|         PowerShell child likely never produced SEED (saw_seed={saw_seed}). This is a \\\n 609|         test-environment failure, not a product regression.\"\n 610|    );\n...\n 617|        max_drained_seq > 0,\n*618|        \"HARNESS: the controller must have drained a real seq>0 frame before \\\n 619|         black-holing (load-gated repro); got max_drained_seq={max_drained_seq}\"\n 620|    );\n 621|    assert!(\n 622|        (1..=2).contains(&stall_evicts_seen),\n*623|        \"HARNESS: the one wedged controller's release must be tallied once (logical \\\n 624|         reap OR physical poison) or twice (both raced — poison-beats-evict, \\\n 625|         field-accepted v0.30.6); got {stall_evicts_seen}\"\n 626|    );\n### control_stamp_lifetime.rs\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 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 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 79|}\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 141|    // ── A REAL broker hosting a LIVE session for THIS endpoint. dispatch_spawn\n*142|    //    pre-attaches the spawner as the LOCAL controller (by=None) and its\n*143|    //    become_controller → stamp_driven_by stamps `controlled=true` on the perch. ──\n 144|    let name = unique_name();\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...\n 150|\n*151|    // Give the spawn's become_controller stamp a beat to land.\n 152|    thread::sleep(Duration::from_millis(200));\n 153|\n 154|    // Precondition (genuine): the live session stamped `controlled=true` (the local\n*155|    // controller). This is a REAL broker stamp, not a hand-set value.\n 156|    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\n 157|    assert!(\n 158|        before.controlled,\n 159|        \"precondition: the live hosted session must stamp controlled=true \\\n*160|         (become_controller → stamp_driven_by) — got {before:?}\"\n 161|    );\n 162|\n 163|    // Simulate the FULL latch a remote-driven, multi-viewer session leaves in\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 217|        after.driven_by, None,\n*218|        \"reap must clear driven_by (a remote-controller latch must not outlive the session)\"\n 219|    );\n 220|    assert!(\n 221|        !after.controlled,\n### dispatch.rs\n 96|/// flag (set on test exit so the loop thread winds down).\n*97|fn spawn_dispatcher_for(broker_name: &str, scratch: &std::path::Path) -> Arc<AtomicBool> {\n 98|    let stop = Arc::new(AtomicBool::new(false));\n 99|    let flag = Arc::clone(&stop);\n 100|    let name = broker_name.to_string();\n...\n 753|            bound: true,\n*754|            controller_node: None,\n 755|            harness_only: false,\n 756|            adapter: None,\n 757|            recent_projects: Vec::new(),\n...\n 1076|            bound: true,\n*1077|            controller_node: None,\n 1078|            harness_only: false,\n 1079|            adapter: None,\n 1080|            recent_projects: Vec::new(),\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 11|//! substrate that self-heals it — the same lesson as the B2\n 12|//! `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE` keystone, which heals the\n...\n 22|//!   `DRIVEN_BY_SELFHEAL_OFFLINE`). RACE-FREE: with no live broker session there is\n*23|//!   no controller to re-stamp it concurrently. The Gap-B test below GREEN-locks\n 24|//!   this. (The original plan expected RED; the fix was already landed on this\n 25|//!   delivery branch — the test pins the GREEN behavior, not the buggy latch.)\n 26|//!\n...\n 29|//!   reconcile MUST NOT clear `driven_by` for a perch that HAS a live session off a\n*30|//!   `controller_by==None` read, because that read is AMBIGUOUS — a live, LOCAL\n*31|//!   controller is pre-attached as `by=None` (`dispatch_spawn` →\n*32|//!   `become_controller(by=None)`), so `controller_by==None` cannot distinguish a\n*33|//!   lost controller from a live local one. A reconcile clear on `None` alone would\n 34|//!   FALSE-CLEAR a genuinely-driven session. The genuine production residual —\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 40|//!   for its empirical characterization.\n 41|//!\n 42|//! ## What this gate OBSERVES (see the eprintln banners)\n 43|//!\n*44|//! The broker's `KIND_SESSIONS` reply carries `SessionInfo.controller_by`\n 45|//! (additive, `#[serde(default)]`). The Gap-A test REPORTS, for the live session,\n*46|//! the exact `controller_by` value the real broker returns, and surfaces the\n 47|//! CRITICAL finding (see the banner + the assertions): a locally-spawned session is\n*48|//! pre-attached as the LOCAL controller with `by=None`, so a LIVE, locally-driven\n*49|//! session ALSO reports `controller_by==None`. `controller_by==None` therefore is\n 50|//! UNUSABLE as a Gap-A clear signal — proving why the reconcile must leave the\n 51|//! live-session latch alone and the real fix is deferred broker-side eviction.\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 100|    #[cfg(windows)]\n*101|    let _ = std::process::Command::new(\"taskkill\")\n 102|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 103|        .output();\n 104|    #[cfg(unix)]\n*105|    let _ = std::process::Command::new(\"kill\")\n 106|        .args([\"-9\", &pid.to_string()])\n 107|        .output();\n 108|}\n...\n 111|/// thread (the existing rig pattern). Returns the broker handle.\n*112|fn start_broker(name: &str) -> Arc<Broker> {\n 113|    let broker = Broker::bind(name).expect(\"bind broker\");\n 114|    let serve = Arc::clone(&broker);\n 115|    thread::spawn(move || {\n### endpoint_survival.rs\n 174|    #[cfg(windows)]\n*175|    let _ = std::process::Command::new(\"taskkill\")\n 176|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 177|        .output();\n 178|    #[cfg(unix)]\n*179|    let _ = std::process::Command::new(\"kill\")\n 180|        .args([\"-9\", &pid.to_string()])\n 181|        .output();\n 182|}\n### false_promote.rs\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 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 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...\n 38|//!\n*39|//! With a ready candidate and the old-gen local controller wedged:\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...\n 90|    #[cfg(windows)]\n*91|    let _ = Command::new(\"taskkill\")\n 92|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 93|        .output();\n 94|    #[cfg(unix)]\n*95|    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\n 96|}\n 97|\n 98|/// A child that FLOODS stdout forever (never reads stdin) — fills a non-reading\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...\n 129|    {\n*130|        Command::new(\"cmd\")\n 131|            .args([\"/C\", \"ping\", \"-n\", \"60\", \"127.0.0.1\"])\n 132|            .stdout(std::process::Stdio::null())\n 133|            .spawn()\n...\n 136|    {\n*137|        Command::new(\"sleep\").arg(\"60\").spawn()\n 138|    }\n 139|}\n 140|\n...\n 188|/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial with a faked readiness\n*189|/// stamp, but the DRAINED gate reads REAL broker truth (`any_local_controller_wedged`)\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...\n 230|    fn old_gen_drained(&self) -> bool {\n*231|        !self.broker.any_local_controller_wedged()\n 232|    }\n 233|}\n 234|\n...\n 250|    // ── Spawner A: spawn the flood child. dispatch_spawn pre-attaches A as the LOCAL\n*251|    //    controller (by = None) — the old-generation-conn identity. ──\n 252|    let mut a = connect(&name);\n 253|    send(\n 254|        &mut a,\n...\n 263|    };\n*264|    // Inject the statically-held wedge (doyle Option C): a LOCAL controller whose\n 265|    // write_blocked_since is already 1 s in the past (> the 400 ms deadline) and\n 266|    // whose writer never touches the socket — the round-4 watchdog has nothing to\n 267|    // cancel, so the wedge persists until the take drains it logically. This\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 273|    );\n 274|    drop(a);\n...\n 278|    assert!(\n*279|        broker.any_local_controller_wedged(),\n*280|        \"precondition: the old-gen local controller conn must read WEDGED before the trial\"\n 281|    );\n 282|\n 283|    // ── Drive the REAL promotion path (supervise_brain → run_trial) with a scripted\n...\n 333|    // ── Drain the old-gen conn: a second operator's take stall-evicts the wedged local\n*334|    //    incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. ──\n 335|    remote_take(&name, sid);\n 336|\n 337|    // Poll for the promotion the drain unblocks (bounded — the gate flips within a tick\n### inject_control_wedge.rs\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...\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 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 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 146|}\n...\n 213|/// which makes b4's full-speed drain hit the dead-operator write-fail →\n*214|/// Disconnected-evict → controller_by clears — the rig bug that made w5_a2 a false\n*215|/// red. A silent child never feeds `controller_writer`, so the abandoned-controller\n 216|/// slot is never cleared via the output path, faithfully staging the IDLE case the\n 217|/// test characterizes. The TRUE idle-evict is deferred to\n 218|/// REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT — the D4c presence oracle.) Unix `sleep`\n...\n 285|#[test]\n*286|fn a_backed_up_controller_does_not_wedge_the_session() {\n 287|    let name = unique_name();\n 288|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 289|    let serve_broker = Arc::clone(&broker);\n...\n 294|    // ── Controller connection A: spawn the flooding child. dispatch_spawn\n*295|    //    pre-attaches A as the CONTROLLER (become_controller, by=None). ──\n 296|    let mut a = None;\n 297|    for _ in 0..200 {\n 298|        match LocalSocketTransport::connect(&name) {\n...\n 305|    }\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...\n 320|    // ── A now STOPS reading. The child floods → A's socket recv buffer fills →\n*321|    //    the controller writer thread blocks on it → the controller channel fills\n 322|    //    → the drain's bounded send_timeout starts blocking. PRE-FIX this parked\n 323|    //    the drain inside append HOLDING the log lock (permanent wedge). POST-FIX\n 324|    //    the send is OFF the log lock, so the session must stay responsive even\n*325|    //    while the controller is fully backed up. Probe DURING the stall (before\n 326|    //    CONTROLLER_WRITE_DEADLINE = 5s elapses). ──\n 327|    thread::sleep(Duration::from_secs(3));\n 328|\n...\n 332|    // the parked session's held lock; POST-FIX it answers because the drain no\n*333|    // longer holds the lock during the controller send.\n 334|    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));\n 335|    let sessions_answered =\n 336|        probe(&name, KIND_SESSIONS, KIND_SESSIONS_REPLY, Duration::from_secs(5));\n...\n 355|        sessions_answered,\n*356|        \"a fully-backed-up controller must NOT wedge the session: KIND_SESSIONS \\\n 357|         (which reads each log's delivered_through under the log lock) must still \\\n*358|         answer because the controller write is delivered OFF the log lock on a \\\n 359|         dedicated writer thread (W1 fix). Pre-fix this hung — the inline \\\n*360|         controller write parked the drain holding the log lock.\"\n 361|    );\n 362|}\n 363|\n...\n 393|    }\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...\n 406|    };\n*407|    // Keep the controller socket drained so append never parks.\n 408|    thread::spawn(move || while read_frame(&mut a).is_ok() {});\n 409|\n 410|    // Inject a LARGE payload into the no-binary session via a SECOND brain\n### input_ack_deadlock.rs\n 14|//! brain's writes block too → mutual full-duplex DEADLOCK. The handler is wedged\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 20|//! applied frame → the broker's per-conn handler NEVER writes back while servicing\n 21|//! the flood → the return direction can't fill → the handler always drains → no\n...\n 27|//!\n*28|//! Revert `serve_attach`'s controller arm to `brain.send_effect(op_id, &bytes)`\n 29|//! (ack=true) — or drop the `if req.ack` gate in `dispatch_input` so it always\n 30|//! sends the applied frame. Then the FLOOD_N-record flood fills the target\n 31|//! brain↔broker return direction; the per-conn handler wedges on `send_frame`, and\n...\n 107|    #[cfg(windows)]\n*108|    let _ = std::process::Command::new(\"taskkill\")\n 109|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 110|        .output();\n 111|    #[cfg(unix)]\n*112|    let _ = std::process::Command::new(\"kill\")\n 113|        .args([\"-9\", &pid.to_string()])\n 114|        .output();\n 115|}\n...\n 121|/// confound this gate with the W1 output-drain hazard (output backing up the\n*122|/// non-draining controller conn), so we deliberately avoid any child output here.\n 123|fn quiet_spawn_req(endpoint: &str) -> SpawnReq {\n 124|    #[cfg(unix)]\n 125|    let (program, args) = (\"sleep\".to_string(), vec![\"600\".to_string()]);\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 287|\n*288|    // ── (A) The FLOOD controller: dial loopback, request a CONTROLLER attach, then\n 289|    //    fire FLOOD_N input records back-to-back on the ONE stream WITHOUT reading.\n 290|    //    The target's serve_attach reads them in one decoder.push() batch and calls\n 291|    //    send_effect_no_ack per record (post-fix). The flood child also floods\n...\n 345|        // The target side serves the inbound attach (the REAL serve_attach pump —\n*346|        // its controller arm drives send_effect_no_ack for every flooded record).\n 347|        let mut target = connect_retry(&flood_name);\n 348|        let (stream_a, origin) = match wait_for_stream(&mut target) {\n 349|            Some(s) => s,\n...\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 379|        // child-kill + broker drop unwinds the abandoned serve thread (serve threads\n...\n 556|         operator's own writes (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Reverting the fix \\\n*557|         (serve_attach's controller arm to send_effect with ack=true, or dropping the \\\n 558|         dispatch_input `if req.ack` gate) re-introduces the deadlock and flips this RED.\"\n 559|    );\n 560|    // (2) The broker PROCESS stays alive on a fresh conn during/after the flood.\n### legacy_resident_sweep_e2e.rs\n 43|#[cfg(windows)]\n*44|fn spawn_marked(marker: &str) -> std::process::Child {\n*45|    std::process::Command::new(\"cmd\")\n 46|        .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n 47|        .spawn()\n 48|        .expect(\"spawn marked child\")\n...\n 52|#[cfg(unix)]\n*53|fn spawn_marked(marker: &str) -> std::process::Child {\n*54|    std::process::Command::new(\"sh\")\n 55|        .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n 56|        .spawn()\n 57|        .expect(\"spawn marked child\")\n### pump.rs\n 355|                    bound: true,\n*356|                    controller_node: None,\n 357|                    harness_only: false,\n 358|                    adapter: None,\n 359|                    recent_projects: Vec::new(),\n### replicate.rs\n 126|        bound: true,\n*127|        controller_node: None,\n 128|        harness_only: false,\n 129|        adapter: None,\n 130|        recent_projects: Vec::new(),\n...\n 165|            bound: true,\n*166|            controller_node: None,\n 167|            harness_only: false,\n 168|            adapter: None,\n 169|            recent_projects: Vec::new(),\n### sync.rs\n 436|            bound: true,\n*437|            controller_node: None,\n 438|            harness_only: false,\n 439|            adapter: None,\n 440|            recent_projects: Vec::new(),\n...\n 453|            bound: true,\n*454|            controller_node: None,\n 455|            harness_only: false,\n 456|            adapter: None,\n 457|            recent_projects: Vec::new(),\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 115|// The B→A store-state signal that the child's control stamps landed at B (a\n 116|// replicated notif row — the rung-7 pattern); A kills the child on seeing it.\n...\n 314|/// (drive half), both against the canonical SPT_HOME stores.\n*315|fn start_loops(\n 316|    broker_name: &str,\n 317|    own_hex: &str,\n 318|    peer_hex: String,\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 880|    // writes fail, the writer thread finishes, and this broker's KIND_SESSIONS\n 881|    // poll (the probe below — production's own reconcile-tick trigger) reaps the\n...\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 1558|    // no conn close, no protocol goodbye — a real ungraceful transport sever.\n*1559|    // B's continuously-ticking session fails the controller writer's next write;\n 1560|    // B asserts the reap + stamp converge.\n 1561|    let sid2_dest = scratch.path().join(\"sid2.txt\");\n 1562|    let end = Instant::now() + rig.wait;\n...\n 1588|    let child_exe = std::env::current_exe().expect(\"current test exe\");\n*1589|    let mut child = std::process::Command::new(&child_exe)\n 1590|        .args([\"b2_attacher_child\", \"--exact\", \"--nocapture\"])\n 1591|        .env(\"SPT_TWO_HOST_B2_CHILD\", \"1\")\n 1592|        .env(\"SPT_TWO_HOST_B2_SID\", sid2.to_string())\n### wake_single_flight.rs\n 54|    #[cfg(windows)]\n*55|    let _ = std::process::Command::new(\"taskkill\")\n 56|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 57|        .output();\n 58|    #[cfg(unix)]\n*59|    let _ = std::process::Command::new(\"kill\")\n 60|        .args([\"-9\", &pid.to_string()])\n 61|        .output();\n 62|}\n\n## spt/tests/\n### active_only_never_relay_e2e.rs\n 35|fn send_bounded(spt_bin: &std::path::Path, home: &std::path::Path, args: &[&str], body: &str) -> Output {\n*36|    let mut child = Command::new(spt_bin)\n 37|        .no_window()\n 38|        .args(args)\n 39|        .env(\"SPT_HOME\", home)\n...\n 83|            bound: true,\n*84|            controller_node: None,\n 85|            harness_only: false,\n 86|            adapter: None,\n 87|            recent_projects: Vec::new(),\n\nShowing files 1-20 of 78+. Use skip=20 for the next page, or narrow paths/pattern.\n\nSkipped missing paths: crates/spt-broker/tests, crates/spt-broker/src"}],"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":204,"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/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/dispatch.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/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/pump.rs","crates/spt-daemon/tests/replicate.rs","crates/spt-daemon/tests/sync.rs","crates/spt-daemon/tests/twohost.rs","crates/spt-daemon/tests/wake_single_flight.rs","crates/spt/tests/active_only_never_relay_e2e.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":8},{"path":"crates/spt-daemon/tests/broker.rs","count":18},{"path":"crates/spt-daemon/tests/budget.rs","count":1},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":19},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":20},{"path":"crates/spt-daemon/tests/dispatch.rs","count":3},{"path":"crates/spt-daemon/tests/driven_by_selfheal.rs","count":20},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":2},{"path":"crates/spt-daemon/tests/false_promote.rs","count":20},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":20},{"path":"crates/spt-daemon/tests/input_ack_deadlock.rs","count":11},{"path":"crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","count":4},{"path":"crates/spt-daemon/tests/pump.rs","count":1},{"path":"crates/spt-daemon/tests/replicate.rs","count":2},{"path":"crates/spt-daemon/tests/sync.rs","count":2},{"path":"crates/spt-daemon/tests/twohost.rs","count":9},{"path":"crates/spt-daemon/tests/wake_single_flight.rs","count":2},{"path":"crates/spt/tests/active_only_never_relay_e2e.rs","count":2}],"truncated":true,"fileLimitReached":20,"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     │...\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  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 1015│    // The clear is now BROKER-owned (REQ-RCVIEW-1, single writer): the worker's\n*1016│    // detach releases the controller slot over IPC, and the broker re-stamps\n 1017│    // driven_by=None asynchronously — so poll for it (production clears the same\n 1018│    // async way when the worker connection drops).\n 1019│    let cleared = (0..200).any(|_| {\n     │...\n 1032│\n*1033│// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n*1034│// These exercise the OutputLog controller/viewer fan-out directly over the local\n 1035│// broker IPC: each `attach_as` carries a distinct `by` identity, so the role\n 1036│// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n 1037│// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n     │...\n 1041│/// Spawn a LABELED echo session on a fresh broker; return (broker, name, brain,\n*1042│/// sid). The spawner is the broker's by=None local controller placeholder.\n 1043│fn labeled_session(label: &str) -> (Arc<Broker>, String, Brain, u64) {\n 1044│    init_home();\n 1045│    let dir = HOME.get().expect(\"home\").path().to_path_buf();\n     │...\n 1069│\n*1070│// [int->REQ-RCVIEW-1] [int->REQ-KICK-1] The controller/viewer/take matrix and the\n 1071│// loud kick over the broker fan-out: a FREE endpoint's first Control becomes\n*1072│// controller; a DIFFERENT operator's Control is BUSY-REFUSED (never a silent\n 1073│// displace, gate #4); a Viewer coexists; an explicit Take displaces the\n*1074│// incumbent with a LOUD `Displaced{by}` and becomes controller.\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 1080│    let mut a = connect_retry(&name);\n 1081│    a.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"A control\");\n*1082│    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, \"free → controller\");\n 1083│\n 1084│    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a\n 1085│    // silent displace (gate #4). A is untouched.\n     │...\n 1090│        SubscribeOutcome::BusyControlled { by: \"node-A\".to_string() },\n*1091│        \"controlled → busy-refuse with the controller's identity\"\n 1092│    );\n 1093│\n 1094│    // A viewer coexists (read-only; never displaces).\n     │...\n 1099│    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes\n*1100│    // controller. This is the ONLY displace path.\n 1101│    b.attach_as(sid, 0, AttachIntent::Take, Some(\"node-B\")).expect(\"B take\");\n 1102│    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, \"take → took control\");\n 1103│    let displaced = (0..2000).find_map(|_| match a.read_event().expect(\"A event\") {\n     │...\n 1110│// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n*1111│// a successor re-taking the controller slot after a brain restart — silently\n 1112│// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n 1113│// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n 1114│// no Displaced reaches the first connection.\n     │...\n 1133│        SubscribeOutcome::Controller,\n*1134│        \"same-origin re-subscribe silently re-takes the controller\"\n 1135│    );\n 1136│\n 1137│    // The first connection must NOT receive a Displaced — re-taking your own slot\n     │...\n 1157│// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a\n*1158│// viewer's connection is refused by the broker (gate #3); the controller's\n 1159│// resize is honored.\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*1165│    // controller's resize is honored ⇒ NO Error frame arrives), and the\n 1166│    // `read_event_until` deadline is only honored on the pump carrier — a Whole\n 1167│    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`\n 1168│    // emits nothing on resize, unlike Windows `findstr`).\n### brain_decouple.rs\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    │...\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    │...\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  85│    #[cfg(windows)]\n *86│    let _ = std::process::Command::new(\"taskkill\")\n  87│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n  88│        .output();\n  89│    #[cfg(unix)]\n *90│    let _ = std::process::Command::new(\"kill\")\n  91│        .args([\"-9\", &pid.to_string()])\n  92│        .output();\n  93│}\n    │...\n  95│/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output case\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    │...\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 237│    // ── Spawner A: spawn the flood child (dispatch_spawn pre-attaches A as the\n*238│    //    LOCAL controller, by=None). ──\n 239│    let mut a = connect(&name);\n 240│    send(\n 241│        &mut a,\n    │...\n 251│\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 257│    //    isolates the fix from the pre-existing \"a remote takes an UNDRIVEN\n    │...\n 287│    });\n*288│    // Give R time to become the controller and its writer to wedge on the flood.\n 289│    thread::sleep(Duration::from_millis(300));\n*290│    // The local spawner is displaced by R's take; drop it (no longer the controller).\n 291│    drop(a);\n 292│\n 293│    // ── Viewer V: a SEPARATE conn that DOES drain. Prove the fan-out keeps ticking\n    │...\n 347│\n*348│    // The viewer kept ticking while the controller conn was wedged.\n 349│    assert!(\n 350│        ticks_during_wedge > 0,\n*351│        \"an attached viewer must keep receiving output while the controller conn is \\\n 352│         wedged — the fan-out must never block on the stalled brain conn\"\n 353│    );\n 354│    // The take COMPLETED and took control (the wedged incumbent was stall-evicted).\n    │...\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 382│// completes. But hertz's RCA (`docs/UPDATE-WEDGE-ROOT-CAUSE.md` @ f8596ca,\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### brain_resume_conn_deadlock.rs\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  65│    #[cfg(windows)]\n *66│    let _ = std::process::Command::new(\"taskkill\")\n  67│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n  68│        .output();\n  69│    #[cfg(unix)]\n *70│    let _ = std::process::Command::new(\"kill\")\n  71│        .args([\"-9\", &pid.to_string()])\n  72│        .output();\n  73│}\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 118│    let mut c = connect(name);\n 119│    write_frame(\n 120│        &mut c,\n    │...\n 195│        pids.push(pid);\n*196│        conns.push(conn); // hold the spawner controller conns open (undrained)\n 197│    }\n 198│    // Let the floods get going so the brain conn genuinely backs up on subscribe.\n 199│    thread::sleep(Duration::from_millis(600));\n    │...\n 302│    // this STRUCTURAL check — we reuse spawn_flood only to host real sessions and hold\n*303│    // the spawner controller conns + pids open, exactly like heartbeat_ticks_under_flood.\n 304│    let mut conns = Vec::new();\n 305│    let mut pids = Vec::new();\n 306│    let mut sids = Vec::new();\n    │...\n 310│        pids.push(pid);\n*311│        conns.push(conn); // hold the spawner controller conns open (undrained)\n 312│    }\n 313│\n 314│    // POSITIVE arm — the SHIPPING daemon resume path: cursor-only, NO subscribe.\n### broker.rs\n  421│#[test]\n *422│fn spawn_env_reaches_child() {\n  423│    env_inject_test();\n  424│}\n  425│\n     │...\n  709│/// interleaved early output into `out`).\n *710│fn spawn_echo(conn: &mut Stream, out: &mut Vec<u8>) -> u64 {\n  711│    send(conn, KIND_SPAWN, serde_json::to_value(echo_spawn_req()).unwrap());\n  712│    loop {\n  713│        let f = read_frame(conn).expect(\"frame before spawned\");\n     │...\n  982│// ── v0.13.0 P1c — REQ-HAZARD-CONTROLLER-WRITER-REORDER ────────────────────────\n *983│// THE INT KEYSTONE: deterministically drive the two-`become_controller`-on-ONE-\n  984│// connection ordering through the PUBLIC broker path (a brain that subscribes\n  985│// from_seq=1, then re-subscribes from_seq=0 over the SAME conn — reproducing the\n  986│// pre-fix handoff(eager subscribe @ prior.next_seq=1) + re-serve(attach_as(sid,0))\n     │...\n  992│// epoch gate) stops a SUPERSEDED writer that has not yet committed, but a frame\n *993│// the stale writer-A committed BEFORE the second `become_controller` bumps the\n *994│// epoch is irreducible at the broker (per the controller_writer doc-comment). Fix\n  995│// #1 removes the contradictory double-take in production; fix #3 makes the\n  996│// consumer dedup-below+snap-above tolerant. The INVARIANT a consumer must always\n  997│// see is therefore: the ACCEPTED seq stream is monotonic non-decreasing and\n     │...\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 1023│\n*1024│    // Spawn the echo child; the broker auto-subscribes this conn as controller.\n 1025│    send(\n 1026│        &mut conn,\n 1027│        KIND_SPAWN,\n     │...\n 1054│                self.accepted_seqs.last().is_none_or(|&last| seq > last),\n*1055│                \"accepted controller seq stream must be strictly forward (monotonic, \\\n 1056│                 exactly-once): seq {seq} arrived after {:?} — a reorder would \\\n 1057│                 surface here as the `got seq 1 want 0` gap\",\n 1058│                self.accepted_seqs\n     │...\n 1109│\n*1110│    // THE RACE: two `become_controller` on this ONE connection, back-to-back —\n*1111│    //   (1) subscribe from_seq=1  → broker become_controller(from_seq=1), initial=[1]\n 1112│    //       spawns writer-A (the pre-fix eager-handoff registration).\n*1113│    //   (2) subscribe from_seq=0  → broker become_controller(from_seq=0), initial=[0..]\n*1114│    //       bumps controller_epoch, spawns writer-B (the re-serve attach_as(sid,0)).\n 1115│    // Both `by:None`+Control = SAME identity → each is a silent re-take (gate #7),\n 1116│    // exactly the brain-restart re-serve shape. Pre-fix writer-A's stale seq 1 can\n 1117│    // win the socket ahead of writer-B's seq 0 → reorder.\n     │...\n 1133│    // ACCEPTED both bounds the read AND proves the session stayed LIVE through the\n*1134│    // double controller swap (input still reaches the PTY, output still flows).\n 1135│    send(\n 1136│        &mut conn,\n 1137│        KIND_INPUT,\n     │...\n 1157│            std::time::Instant::now() < deadline,\n*1158│            \"liveness marker P1C_LIVE must arrive through the re-served controller \\\n 1159│             within the watchdog — a serve thread that died on a gap (the reorder \\\n 1160│             HANG face) never re-pumps it. accepted={:?}\",\n 1161│            dedup.accepted_seqs\n     │...\n 1175│        dedup.accepted_seqs.windows(2).all(|w| w[0] < w[1]),\n*1176│        \"the accepted controller seq stream is strictly increasing end-to-end \\\n*1177│         (monotonic + exactly-once across the double become_controller): {:?}\",\n 1178│        dedup.accepted_seqs\n 1179│    );\n 1180│    let mut full = ground.clone();\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### budget.rs\n 219│fn sample_pid(pid: u32) -> Option<Sample> {\n*220│    let out = std::process::Command::new(\"powershell\")\n 221│        .args([\n 222│            \"-NoProfile\",\n 223│            \"-Command\",\n### conn_blackhole_lifecycle.rs\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 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 118│}\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 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 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 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 576│    //    ADVANCED across the whole incident window (baseline → now). It kept\n*577│    //    receiving output while B's controller conn was black-holed + retired. ──\n 578│    let u_final = u_frames.load(Ordering::Relaxed);\n 579│\n 580│    // ── Teardown BEFORE asserting (so a failing assert leaves no orphan child):\n    │...\n 606│        old_client_write_is_err.is_some(),\n*607│        \"HARNESS: the black-holed controller never handed its stream back — the \\\n 608│         PowerShell child likely never produced SEED (saw_seed={saw_seed}). This is a \\\n 609│         test-environment failure, not a product regression.\"\n 610│    );\n    │...\n 617│        max_drained_seq > 0,\n*618│        \"HARNESS: the controller must have drained a real seq>0 frame before \\\n 619│         black-holing (load-gated repro); got max_drained_seq={max_drained_seq}\"\n 620│    );\n 621│    assert!(\n 622│        (1..=2).contains(&stall_evicts_seen),\n*623│        \"HARNESS: the one wedged controller's release must be tallied once (logical \\\n 624│         reap OR physical poison) or twice (both raced — poison-beats-evict, \\\n 625│         field-accepted v0.30.6); got {stall_evicts_seen}\"\n 626│    );\n### control_stamp_lifetime.rs\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  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  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  79│}\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 141│    // ── A REAL broker hosting a LIVE session for THIS endpoint. dispatch_spawn\n*142│    //    pre-attaches the spawner as the LOCAL controller (by=None) and its\n*143│    //    become_controller → stamp_driven_by stamps `controlled=true` on the perch. ──\n 144│    let name = unique_name();\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    │...\n 150│\n*151│    // Give the spawn's become_controller stamp a beat to land.\n 152│    thread::sleep(Duration::from_millis(200));\n 153│\n 154│    // Precondition (genuine): the live session stamped `controlled=true` (the local\n*155│    // controller). This is a REAL broker stamp, not a hand-set value.\n 156│    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\n 157│    assert!(\n 158│        before.controlled,\n 159│        \"precondition: the live hosted session must stamp controlled=true \\\n*160│         (become_controller → stamp_driven_by) — got {before:?}\"\n 161│    );\n 162│\n 163│    // Simulate the FULL latch a remote-driven, multi-viewer session leaves in\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 217│        after.driven_by, None,\n*218│        \"reap must clear driven_by (a remote-controller latch must not outlive the session)\"\n 219│    );\n 220│    assert!(\n 221│        !after.controlled,\n### dispatch.rs\n   96│/// flag (set on test exit so the loop thread winds down).\n  *97│fn spawn_dispatcher_for(broker_name: &str, scratch: &std::path::Path) -> Arc<AtomicBool> {\n   98│    let stop = Arc::new(AtomicBool::new(false));\n   99│    let flag = Arc::clone(&stop);\n  100│    let name = broker_name.to_string();\n     │...\n  753│            bound: true,\n *754│            controller_node: None,\n  755│            harness_only: false,\n  756│            adapter: None,\n  757│            recent_projects: Vec::new(),\n     │...\n 1076│            bound: true,\n*1077│            controller_node: None,\n 1078│            harness_only: false,\n 1079│            adapter: None,\n 1080│            recent_projects: Vec::new(),\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  11│//! substrate that self-heals it — the same lesson as the B2\n  12│//! `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE` keystone, which heals the\n    │...\n  22│//!   `DRIVEN_BY_SELFHEAL_OFFLINE`). RACE-FREE: with no live broker session there is\n *23│//!   no controller to re-stamp it concurrently. The Gap-B test below GREEN-locks\n  24│//!   this. (The original plan expected RED; the fix was already landed on this\n  25│//!   delivery branch — the test pins the GREEN behavior, not the buggy latch.)\n  26│//!\n    │...\n  29│//!   reconcile MUST NOT clear `driven_by` for a perch that HAS a live session off a\n *30│//!   `controller_by==None` read, because that read is AMBIGUOUS — a live, LOCAL\n *31│//!   controller is pre-attached as `by=None` (`dispatch_spawn` →\n *32│//!   `become_controller(by=None)`), so `controller_by==None` cannot distinguish a\n *33│//!   lost controller from a live local one. A reconcile clear on `None` alone would\n  34│//!   FALSE-CLEAR a genuinely-driven session. The genuine production residual —\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  40│//!   for its empirical characterization.\n  41│//!\n  42│//! ## What this gate OBSERVES (see the eprintln banners)\n  43│//!\n *44│//! The broker's `KIND_SESSIONS` reply carries `SessionInfo.controller_by`\n  45│//! (additive, `#[serde(default)]`). The Gap-A test REPORTS, for the live session,\n *46│//! the exact `controller_by` value the real broker returns, and surfaces the\n  47│//! CRITICAL finding (see the banner + the assertions): a locally-spawned session is\n *48│//! pre-attached as the LOCAL controller with `by=None`, so a LIVE, locally-driven\n *49│//! session ALSO reports `controller_by==None`. `controller_by==None` therefore is\n  50│//! UNUSABLE as a Gap-A clear signal — proving why the reconcile must leave the\n  51│//! live-session latch alone and the real fix is deferred broker-side eviction.\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 100│    #[cfg(windows)]\n*101│    let _ = std::process::Command::new(\"taskkill\")\n 102│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 103│        .output();\n 104│    #[cfg(unix)]\n*105│    let _ = std::process::Command::new(\"kill\")\n 106│        .args([\"-9\", &pid.to_string()])\n 107│        .output();\n 108│}\n    │...\n 111│/// thread (the existing rig pattern). Returns the broker handle.\n*112│fn start_broker(name: &str) -> Arc<Broker> {\n 113│    let broker = Broker::bind(name).expect(\"bind broker\");\n 114│    let serve = Arc::clone(&broker);\n 115│    thread::spawn(move || {\n### endpoint_survival.rs\n 174│    #[cfg(windows)]\n*175│    let _ = std::process::Command::new(\"taskkill\")\n 176│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 177│        .output();\n 178│    #[cfg(unix)]\n*179│    let _ = std::process::Command::new(\"kill\")\n 180│        .args([\"-9\", &pid.to_string()])\n 181│        .output();\n 182│}\n### false_promote.rs\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  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  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    │...\n  38│//!\n *39│//! With a ready candidate and the old-gen local controller wedged:\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    │...\n  90│    #[cfg(windows)]\n *91│    let _ = Command::new(\"taskkill\")\n  92│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n  93│        .output();\n  94│    #[cfg(unix)]\n *95│    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\n  96│}\n  97│\n  98│/// A child that FLOODS stdout forever (never reads stdin) — fills a non-reading\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    │...\n 129│    {\n*130│        Command::new(\"cmd\")\n 131│            .args([\"/C\", \"ping\", \"-n\", \"60\", \"127.0.0.1\"])\n 132│            .stdout(std::process::Stdio::null())\n 133│            .spawn()\n    │...\n 136│    {\n*137│        Command::new(\"sleep\").arg(\"60\").spawn()\n 138│    }\n 139│}\n 140│\n    │...\n 188│/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial with a faked readiness\n*189│/// stamp, but the DRAINED gate reads REAL broker truth (`any_local_controller_wedged`)\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    │...\n 230│    fn old_gen_drained(&self) -> bool {\n*231│        !self.broker.any_local_controller_wedged()\n 232│    }\n 233│}\n 234│\n    │...\n 250│    // ── Spawner A: spawn the flood child. dispatch_spawn pre-attaches A as the LOCAL\n*251│    //    controller (by = None) — the old-generation-conn identity. ──\n 252│    let mut a = connect(&name);\n 253│    send(\n 254│        &mut a,\n    │...\n 263│    };\n*264│    // Inject the statically-held wedge (doyle Option C): a LOCAL controller whose\n 265│    // write_blocked_since is already 1 s in the past (> the 400 ms deadline) and\n 266│    // whose writer never touches the socket — the round-4 watchdog has nothing to\n 267│    // cancel, so the wedge persists until the take drains it logically. This\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 273│    );\n 274│    drop(a);\n    │...\n 278│    assert!(\n*279│        broker.any_local_controller_wedged(),\n*280│        \"precondition: the old-gen local controller conn must read WEDGED before the trial\"\n 281│    );\n 282│\n 283│    // ── Drive the REAL promotion path (supervise_brain → run_trial) with a scripted\n    │...\n 333│    // ── Drain the old-gen conn: a second operator's take stall-evicts the wedged local\n*334│    //    incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. ──\n 335│    remote_take(&name, sid);\n 336│\n 337│    // Poll for the promotion the drain unblocks (bounded — the gate flips within a tick\n### inject_control_wedge.rs\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    │...\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 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 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 146│}\n    │...\n 213│/// which makes b4's full-speed drain hit the dead-operator write-fail →\n*214│/// Disconnected-evict → controller_by clears — the rig bug that made w5_a2 a false\n*215│/// red. A silent child never feeds `controller_writer`, so the abandoned-controller\n 216│/// slot is never cleared via the output path, faithfully staging the IDLE case the\n 217│/// test characterizes. The TRUE idle-evict is deferred to\n 218│/// REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT — the D4c presence oracle.) Unix `sleep`\n    │...\n 285│#[test]\n*286│fn a_backed_up_controller_does_not_wedge_the_session() {\n 287│    let name = unique_name();\n 288│    let broker = Broker::bind(&name).expect(\"bind broker\");\n 289│    let serve_broker = Arc::clone(&broker);\n    │...\n 294│    // ── Controller connection A: spawn the flooding child. dispatch_spawn\n*295│    //    pre-attaches A as the CONTROLLER (become_controller, by=None). ──\n 296│    let mut a = None;\n 297│    for _ in 0..200 {\n 298│        match LocalSocketTransport::connect(&name) {\n    │...\n 305│    }\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    │...\n 320│    // ── A now STOPS reading. The child floods → A's socket recv buffer fills →\n*321│    //    the controller writer thread blocks on it → the controller channel fills\n 322│    //    → the drain's bounded send_timeout starts blocking. PRE-FIX this parked\n 323│    //    the drain inside append HOLDING the log lock (permanent wedge). POST-FIX\n 324│    //    the send is OFF the log lock, so the session must stay responsive even\n*325│    //    while the controller is fully backed up. Probe DURING the stall (before\n 326│    //    CONTROLLER_WRITE_DEADLINE = 5s elapses). ──\n 327│    thread::sleep(Duration::from_secs(3));\n 328│\n    │...\n 332│    // the parked session's held lock; POST-FIX it answers because the drain no\n*333│    // longer holds the lock during the controller send.\n 334│    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));\n 335│    let sessions_answered =\n 336│        probe(&name, KIND_SESSIONS, KIND_SESSIONS_REPLY, Duration::from_secs(5));\n    │...\n 355│        sessions_answered,\n*356│        \"a fully-backed-up controller must NOT wedge the session: KIND_SESSIONS \\\n 357│         (which reads each log's delivered_through under the log lock) must still \\\n*358│         answer because the controller write is delivered OFF the log lock on a \\\n 359│         dedicated writer thread (W1 fix). Pre-fix this hung — the inline \\\n*360│         controller write parked the drain holding the log lock.\"\n 361│    );\n 362│}\n 363│\n    │...\n 393│    }\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    │...\n 406│    };\n*407│    // Keep the controller socket drained so append never parks.\n 408│    thread::spawn(move || while read_frame(&mut a).is_ok() {});\n 409│\n 410│    // Inject a LARGE payload into the no-binary session via a SECOND brain\n### input_ack_deadlock.rs\n  14│//! brain's writes block too → mutual full-duplex DEADLOCK. The handler is wedged\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  20│//! applied frame → the broker's per-conn handler NEVER writes back while servicing\n  21│//! the flood → the return direction can't fill → the handler always drains → no\n    │...\n  27│//!\n *28│//! Revert `serve_attach`'s controller arm to `brain.send_effect(op_id, &bytes)`\n  29│//! (ack=true) — or drop the `if req.ack` gate in `dispatch_input` so it always\n  30│//! sends the applied frame. Then the FLOOD_N-record flood fills the target\n  31│//! brain↔broker return direction; the per-conn handler wedges on `send_frame`, and\n    │...\n 107│    #[cfg(windows)]\n*108│    let _ = std::process::Command::new(\"taskkill\")\n 109│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 110│        .output();\n 111│    #[cfg(unix)]\n*112│    let _ = std::process::Command::new(\"kill\")\n 113│        .args([\"-9\", &pid.to_string()])\n 114│        .output();\n 115│}\n    │...\n 121│/// confound this gate with the W1 output-drain hazard (output backing up the\n*122│/// non-draining controller conn), so we deliberately avoid any child output here.\n 123│fn quiet_spawn_req(endpoint: &str) -> SpawnReq {\n 124│    #[cfg(unix)]\n 125│    let (program, args) = (\"sleep\".to_string(), vec![\"600\".to_string()]);\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 287│\n*288│    // ── (A) The FLOOD controller: dial loopback, request a CONTROLLER attach, then\n 289│    //    fire FLOOD_N input records back-to-back on the ONE stream WITHOUT reading.\n 290│    //    The target's serve_attach reads them in one decoder.push() batch and calls\n 291│    //    send_effect_no_ack per record (post-fix). The flood child also floods\n    │...\n 345│        // The target side serves the inbound attach (the REAL serve_attach pump —\n*346│        // its controller arm drives send_effect_no_ack for every flooded record).\n 347│        let mut target = connect_retry(&flood_name);\n 348│        let (stream_a, origin) = match wait_for_stream(&mut target) {\n 349│            Some(s) => s,\n    │...\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 379│        // child-kill + broker drop unwinds the abandoned serve thread (serve threads\n    │...\n 556│         operator's own writes (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Reverting the fix \\\n*557│         (serve_attach's controller arm to send_effect with ack=true, or dropping the \\\n 558│         dispatch_input `if req.ack` gate) re-introduces the deadlock and flips this RED.\"\n 559│    );\n 560│    // (2) The broker PROCESS stays alive on a fresh conn during/after the flood.\n### legacy_resident_sweep_e2e.rs\n 43│#[cfg(windows)]\n*44│fn spawn_marked(marker: &str) -> std::process::Child {\n*45│    std::process::Command::new(\"cmd\")\n 46│        .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n 47│        .spawn()\n 48│        .expect(\"spawn marked child\")\n   │...\n 52│#[cfg(unix)]\n*53│fn spawn_marked(marker: &str) -> std::process::Child {\n*54│    std::process::Command::new(\"sh\")\n 55│        .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n 56│        .spawn()\n 57│        .expect(\"spawn marked child\")\n### pump.rs\n 355│                    bound: true,\n*356│                    controller_node: None,\n 357│                    harness_only: false,\n 358│                    adapter: None,\n 359│                    recent_projects: Vec::new(),\n### replicate.rs\n 126│        bound: true,\n*127│        controller_node: None,\n 128│        harness_only: false,\n 129│        adapter: None,\n 130│        recent_projects: Vec::new(),\n    │...\n 165│            bound: true,\n*166│            controller_node: None,\n 167│            harness_only: false,\n 168│            adapter: None,\n 169│            recent_projects: Vec::new(),\n### sync.rs\n 436│            bound: true,\n*437│            controller_node: None,\n 438│            harness_only: false,\n 439│            adapter: None,\n 440│            recent_projects: Vec::new(),\n    │...\n 453│            bound: true,\n*454│            controller_node: None,\n 455│            harness_only: false,\n 456│            adapter: None,\n 457│            recent_projects: Vec::new(),\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  115│// The B→A store-state signal that the child's control stamps landed at B (a\n  116│// replicated notif row — the rung-7 pattern); A kills the child on seeing it.\n     │...\n  314│/// (drive half), both against the canonical SPT_HOME stores.\n *315│fn start_loops(\n  316│    broker_name: &str,\n  317│    own_hex: &str,\n  318│    peer_hex: String,\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  880│    // writes fail, the writer thread finishes, and this broker's KIND_SESSIONS\n  881│    // poll (the probe below — production's own reconcile-tick trigger) reaps the\n     │...\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 1558│    // no conn close, no protocol goodbye — a real ungraceful transport sever.\n*1559│    // B's continuously-ticking session fails the controller writer's next write;\n 1560│    // B asserts the reap + stamp converge.\n 1561│    let sid2_dest = scratch.path().join(\"sid2.txt\");\n 1562│    let end = Instant::now() + rig.wait;\n     │...\n 1588│    let child_exe = std::env::current_exe().expect(\"current test exe\");\n*1589│    let mut child = std::process::Command::new(&child_exe)\n 1590│        .args([\"b2_attacher_child\", \"--exact\", \"--nocapture\"])\n 1591│        .env(\"SPT_TWO_HOST_B2_CHILD\", \"1\")\n 1592│        .env(\"SPT_TWO_HOST_B2_SID\", sid2.to_string())\n### wake_single_flight.rs\n 54│    #[cfg(windows)]\n*55│    let _ = std::process::Command::new(\"taskkill\")\n 56│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 57│        .output();\n 58│    #[cfg(unix)]\n*59│    let _ = std::process::Command::new(\"kill\")\n 60│        .args([\"-9\", &pid.to_string()])\n 61│        .output();\n 62│}\n\n## spt/tests/\n### active_only_never_relay_e2e.rs\n 35│fn send_bounded(spt_bin: &std::path::Path, home: &std::path::Path, args: &[&str], body: &str) -> Output {\n*36│    let mut child = Command::new(spt_bin)\n 37│        .no_window()\n 38│        .args(args)\n 39│        .env(\"SPT_HOME\", home)\n   │...\n 83│            bound: true,\n*84│            controller_node: None,\n 85│            harness_only: false,\n 86│            adapter: None,\n 87│            recent_projects: Vec::new(),","missingPaths":["crates/spt-broker/tests","crates/spt-broker/src"]},"isError":false,"timestamp":1784195751587}}
{"type":"message","id":"a1753a5d","parentId":"7678b4d8","timestamp":"2026-07-16T09:55:51.626Z","message":{"role":"toolResult","toolCallId":"call_fK3L0CSqHzwA9RccY96WUDJt|fc_0b42584a8ab0545d016a58aaa77ad08199a616c40090b39000","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\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-223 of 883. Use :224 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","truncated":true,"truncatedBy":"lines","totalLines":883,"totalBytes":56257,"outputLines":223,"outputBytes":56257,"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\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,null,227,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":56257,"outputLines":223,"outputBytes":56257,"shownRange":{"start":1,"end":223},"nextOffset":224},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195751626}}
{"type":"message","id":"9541e73d","parentId":"a1753a5d","timestamp":"2026-07-16T09:55:51.665Z","message":{"role":"toolResult","toolCallId":"call_64HdIMezdhpONggm79LdvYrp|fc_0b42584a8ab0545d016a58aaa77ae081999e21dfe9c977a3d1","toolName":"read","content":[{"type":"text","text":"## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\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…\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- **Source:** MSG-IDENTITY W3 (doyle triage, code-grounded ingest.rs:156 gate vs :200 delete; repro fixture F-032-commune-2026-07-08T222721Z.md).\n\n---\n\n## Conformance checklist (condensed)\n\n| # | Invariant | spt-core surface |\n|---|---|---|\n| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |\n| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |\n| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |\n| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |\n| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |\n| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |\n| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |\n| 4.1 | Envelope decode order, `&amp;` last | spt-proto (public, wire-versioned) |\n| 4.2 | Parser panic-free + tolerant | spt-proto |\n| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |\n| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |\n| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |\n| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |\n| 5.3 | Timeout every harness subprocess | manifest invocations |\n| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |\n| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |\n| 7.7 | A wedged viewer is evicted, never stalls the controller/child/drain | broker `OutputLog` fan-out (controller/viewer) |\n| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |\n| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |\n| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |\n| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |\n| 6.1 | Single path/registry source of truth | storage layout |\n| 6.4 | Drop files supervisor-owned single-writer | runtime contract |\n| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |\n| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |\n| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |\n| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |\n| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |\n| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |\n| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |\n| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |\n| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |\n| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |\n| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |\n| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |\n| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |\n| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not \"failed to fill whole buffer\"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |\n| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent …\n| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\n| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\n| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\n| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |\n| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |\n| 7.17 | PTY **input** is single-writer: each session spawns ONE dedicated input-writer thread = the SOLE caller of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`). Every caller (`dispatch_input`, `dispatch_endpoint_input`, the inject worker/floor flush) ENQUEUES (`try_send`) + returns at once — a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. Full queue ⇒ DROP excess + stamp the perch `input_backpressure` (heal-on-resume: cleared on the next accepted enqueue); the daemon NEVER wedges on a stuck harness. Completes the W1b-deferred fix (2) of 7.14. The OUTPUT-side single-writer (7.12) mirror, applied to input | broker `InputWriter`/`input_writer`/`flush_inject_floor`/`run_inject_wor…\n…\n| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-safe-decline (pid-alive AND basename==psyche-program AND cmdline contains `<id>-psyche`; any unreadable signal DECLINES — a missed dup is bounded, a wrong-kill catastrophic). The orphan-leak half of perri F-010xF-015 (the psyche own-copy is the other half, ADR-…\n…\n<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->\n\n[Showing lines 623-713 of 736. Use :714 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"\r\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]`\r\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\r\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.\r\n\r\n---\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\r\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`.\r\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).\r\n\r\n---\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE] -->\r\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.\r\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).\r\n- **Source:** MSG-IDENTITY W3 (doyle triage, code-grounded ingest.rs:156 gate vs :200 delete; repro fixture F-032-commune-2026-07-08T222721Z.md).\r\n\r\n---\r\n\r\n## Conformance checklist (condensed)\r\n\r\n| # | Invariant | spt-core surface |\r\n|---|---|---|\r\n| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |\r\n| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |\r\n| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |\r\n| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |\r\n| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |\r\n| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |\r\n| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |\r\n| 4.1 | Envelope decode order, `&amp;` last | spt-proto (public, wire-versioned) |\r\n| 4.2 | Parser panic-free + tolerant | spt-proto |\r\n| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |\r\n| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |\r\n| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |\r\n| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |\r\n| 5.3 | Timeout every harness subprocess | manifest invocations |\r\n| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |\r\n| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |\r\n| 7.7 | A wedged viewer is evicted, never stalls the controller/child/drain | broker `OutputLog` fan-out (controller/viewer) |\r\n| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |\r\n| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |\r\n| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |\r\n| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |\r\n| 6.1 | Single path/registry source of truth | storage layout |\r\n| 6.4 | Drop files supervisor-owned single-writer | runtime contract |\r\n| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |\r\n| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |\r\n| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |\r\n| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |\r\n| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |\r\n| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |\r\n| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |\r\n| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |\r\n| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |\r\n| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |\r\n| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |\r\n| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |\r\n| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |\r\n| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not \"failed to fill whole buffer\"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |\r\n| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent …\n| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\r\n| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\r\n| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\r\n| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\r\n| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |\r\n| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |\r\n| 7.17 | PTY **input** is single-writer: each session spawns ONE dedicated input-writer thread = the SOLE caller of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`). Every caller (`dispatch_input`, `dispatch_endpoint_input`, the inject worker/floor flush) ENQUEUES (`try_send`) + returns at once — a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. Full queue ⇒ DROP excess + stamp the perch `input_backpressure` (heal-on-resume: cleared on the next accepted enqueue); the daemon NEVER wedges on a stuck harness. Completes the W1b-deferred fix (2) of 7.14. The OUTPUT-side single-writer (7.12) mirror, applied to input | broker `InputWriter`/`input_writer`/`flush_inject_floor`/`run_inject_wor…","truncated":true,"truncatedBy":"lines","totalLines":114,"totalBytes":29582,"outputLines":91,"outputBytes":29582,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\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…\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\r\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]`\r\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\r\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.\r\n\r\n---\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\r\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`.\r\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).\r\n\r\n---\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE] -->\r\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.\r\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).\r\n- **Source:** MSG-IDENTITY W3 (doyle triage, code-grounded ingest.rs:156 gate vs :200 delete; repro fixture F-032-commune-2026-07-08T222721Z.md).\r\n\r\n---\r\n\r\n## Conformance checklist (condensed)\r\n\r\n| # | Invariant | spt-core surface |\r\n|---|---|---|\r\n| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |\r\n| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |\r\n| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |\r\n| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |\r\n| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |\r\n| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |\r\n| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |\r\n| 4.1 | Envelope decode order, `&amp;` last | spt-proto (public, wire-versioned) |\r\n| 4.2 | Parser panic-free + tolerant | spt-proto |\r\n| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |\r\n| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |\r\n| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |\r\n| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |\r\n| 5.3 | Timeout every harness subprocess | manifest invocations |\r\n| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |\r\n| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |\r\n| 7.7 | A wedged viewer is evicted, never stalls the controller/child/drain | broker `OutputLog` fan-out (controller/viewer) |\r\n| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |\r\n| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |\r\n| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |\r\n| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |\r\n| 6.1 | Single path/registry source of truth | storage layout |\r\n| 6.4 | Drop files supervisor-owned single-writer | runtime contract |\r\n| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |\r\n| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |\r\n| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |\r\n| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |\r\n| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |\r\n| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |\r\n| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |\r\n| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |\r\n| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |\r\n| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |\r\n| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |\r\n| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |\r\n| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |\r\n| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not \"failed to fill whole buffer\"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |\r\n| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent …\n| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\r\n| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\r\n| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\r\n| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\r\n| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |\r\n| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |\r\n| 7.17 | PTY **input** is single-writer: each session spawns ONE dedicated input-writer thread = the SOLE caller of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`). Every caller (`dispatch_input`, `dispatch_endpoint_input`, the inject worker/floor flush) ENQUEUES (`try_send`) + returns at once — a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. Full queue ⇒ DROP excess + stamp the perch `input_backpressure` (heal-on-resume: cleared on the next accepted enqueue); the daemon NEVER wedges on a stuck harness. Completes the W1b-deferred fix (2) of 7.14. The OUTPUT-side single-writer (7.12) mirror, applied to input | broker `InputWriter`/`input_writer`/`flush_inject_floor`/`run_inject_wor…\n…\n| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-safe-decline (pid-alive AND basename==psyche-program AND cmdline contains `<id>-psyche`; any unreadable signal DECLINES — a missed dup is bounded, a wrong-kill catastrophic). The orphan-leak half of perri F-010xF-015 (the psyche own-copy is the other half, ADR-…\n…\n<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->","startLine":365,"lineNumbers":[365,null,618,619,null,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,null,718,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":29582,"outputLines":91,"outputBytes":29582,"shownRange":{"start":623,"end":713},"nextOffset":714},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195751664}}
{"type":"message","id":"34a07b2d","parentId":"9541e73d","timestamp":"2026-07-16T09:56:05.874Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying real-process test harness candidates**"},{"type":"toolCall","id":"call_39zmaBbIZkS1OV8uCR1VyBs1|fc_0b42584a8ab0545d016a58aab5ca388199aee7eb703ea3a406","name":"read","arguments":{"i":"Reading endpoint survival harness","path":"crates/spt-daemon/tests/endpoint_survival.rs","selector":"1-430"}},{"type":"toolCall","id":"call_0pkOtncfL8vt3F72R8moAHbC|fc_0b42584a8ab0545d016a58aab5ca4c8199b6aee68125aefde1","name":"read","arguments":{"i":"Reading process survival harness","path":"crates/spt/tests/brain_survive.rs","selector":"1-420"}},{"type":"toolCall","id":"call_FLKOO7dRUl2x1Fa8BNf7Ok6F|fc_0b42584a8ab0545d016a58aab5ca508199a831e9f7e4505205","name":"read","arguments":{"i":"Reading brain split E2E","path":"crates/spt/tests/brain_split.rs","selector":"1-430"}},{"type":"toolCall","id":"call_7NrzgtQlMN48AzK0cRvJhWip|fc_0b42584a8ab0545d016a58aab5ca58819981fd48cd8fd9ea13","name":"read","arguments":{"i":"Reading daemon lifecycle restart test","path":"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","selector":"1-420"}},{"type":"toolCall","id":"call_TAkuO7g8hA9ixaX7htYFZZSH|fc_0b42584a8ab0545d016a58aab5ca5c81998f452ad0b22ac19e","name":"read","arguments":{"i":"Reading daemon E2E flow","path":"crates/spt-daemon/tests/daemon_e2e.rs","selector":"1-360"}},{"type":"toolCall","id":"call_bnIkvsbGFtZKi5CCAPc7IHcv|fc_0b42584a8ab0545d016a58aab5ca608199bd1fce33d3ff03b4","name":"read","arguments":{"i":"Reading attach restart flow","path":"crates/spt-daemon/tests/attach.rs","selector":"740-880,1030-1405"}},{"type":"toolCall","id":"call_ELEW6U5iSUDbETefjzpxChMW|fc_0b42584a8ab0545d016a58aab5ca688199a5dc09e9e3146079","name":"read","arguments":{"i":"Reading refresh root cause history","path":"docs/UPDATE-WEDGE-ROOT-CAUSE.md","selector":"1-320"}},{"type":"toolCall","id":"call_qsT7irDnwTv10LOqNTaE2H2T|fc_0b42584a8ab0545d016a58aab5ca6c819986ab852ec3b3188d","name":"grep","arguments":{"i":"Finding refresh stall history","pattern":"refresh|update apply|spt update|detach|reattach|rc|stall|most.*PTY|session PTY|controlled session","path":"docs/UPDATE-WEDGE-ROOT-CAUSE.md;docs/KNOWN-HAZARDS.md;docs/FAULT-MATRIX.md;docs/adr/0018-broker-brain-process-isolation-restoration.md;crates/spt/tests;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":41551,"output":554,"cacheRead":33280,"cacheWrite":0,"totalTokens":75385,"reasoningTokens":141,"cost":{"input":0.20775500000000002,"output":0.01662,"cacheRead":0.01664,"cacheWrite":0,"total":0.241015}},"stopReason":"toolUse","timestamp":1784195751716,"responseId":"resp_0b42584a8ab0545d016a58aaa831c08199b8e34f6de516378f","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aaabcd908199a356b5f2669a6379","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKq1Qiztr2qcKy2CXBNT7mWlc8fjz9SUhTDNVDQA_XeR2tVcRkd2fE1FIbKWgroimgdThYeTJu8OgYS4S1FPvrApzT3yAvB6bE7YKGxOnXWkN2cVEl_Y5-2s0q0MngYuPGuPAYBIWugUcdaHmBHL_bbEOnaUxyyQD8TGlJyDb6Y8IakP2QGGrvxzw2ZtM2sUYhHN_clsbf325pMd8dSmX3gauryXLU-GTo2FPAq6plQnmSIjJwNPoX_3QsapL2IV34mwNJnhpKDMhlUPrBKJ2ji81c7T6zD3B7eRSkaderEcyEeBiTYVB_Bbv6AUByiAYQK4nKj66UXWnvs-qqNStmUtK7jll2q6ZBmDoOFb3-v4C2ck-DqbvS5fJOhS2nT1UiWq0Cvbvfx8oGuSFErWgSCJeJ-Al8Z1iCy0jLsEeOc1Iavs6zzDFZPdfjJN6sDuYIjKfxHzRyEJBBKnp9iUz3N4WNHO1vgFooc1IjCNBi17Vz3rNVNCpiGmY9ot2s_iNN8-UtmKUvPTaXM468QJkUqV_Wt6FZ5eNv5UCtaTO3u-cR_YeIlzcIow92MhSBJh6BczqDbOkLxwDELq10TI3EmTY1rE_C4I8LyQETX5WJnyJuzdXJ92xZ0vPcm0iGMlCbMagzCShJgUkMLdnm7FTb6hLW-A39Ho3c66TeQQpS07vYaIoGdoA2Od6eL1-nOCqFFSo3R1sfnXNESRFVuGvC1_OvLoYcsrxWAM4TIGw4J0J4JJ7OTRTJrXKgxHEHCoVJYlr0kBb2-kbGQgyIMxs5WW-XWx-t7Ax4Uj1MxteVJJHq2LcxPlf89iukrg53PLshyePovk2-kW2I5SldAQx3vjwe-mucAfU7QnDWg11liel9tzXkueg1ZmqGfpjj_RK0apXkcqMu54FXRft8l67BTBN-twLESUkRYamoaepMqzZSWbtFFNKK9VBNoi83b0uT-6dX8sOG33mHwPMSLxPfqx7d8ijDxrZdC8CS964xGoNK8XzquPFg5rev-77q8LlmWI0Smofib5e1W_aX1vypky6nMSdNR9Pzu7BMB8psQckiGajEZilOgJqCi5hYwJffnFHqds-bXPSMcxwBWPeRCGkGVqyk_o9gLV6hFbg7BBmBHJbVqF1NOHl5_ynLutMA-IDQvwaRcw9-8n3pkFUN1zeqS_75sACK1q3IB7MsQHf5RZYl4Eryqurvfcnpidm0JuMX3barDJdWuwwsMRKY2On0vJFdT9BkMLVzjp8-vG6Udi_gqVF3W01DmbM88v7XR7uIVeULutF7QNCzBY4tiI1BdiFFsjK7tjADjBkjZ-O-zO7jFapW9w-WEddt9A3DbT_TsRtBSkyDkc-xWH8XipufHgo2SRBuQRa4HOrbQtO6R7CA4LL9YHUnlNkS9cXvMYCXYmcFuYC7E8efi0EJOGSHhekyDyhmjIY4PihsWIn2CdWy7VnL5Z63CcABci-GBiahaz7nNm9FfUmRang9J9dczV0UfjS1k4V_hw55hffAe35Dfs1b6Qzvx_dOU9MZKwre0pFjdcURPVJBKjvQb0Le4LXHGtpOMDfE8B3wIKLqKf54SgGHChS2diM1NCKvnFEkyInvKIT49UZz9qdy7uf9OFzz8JW1_k0gr2aXiU32rPb6eeg1W-5V-LIP8wb5AuNMT9UfU1cy46uDdD5HrBhqUuAXWHtpTXxuH021WQCLUFQQNC7QOke1fmAnicuYYpkbuap96C1nwREeUfkBY45P1Z_5SKvDLodrbKXNbwgvrPnry2rbJ7x3zWOTRJODi-R3OgoMvZIW6B9pidGxJ3B9l44LQbWTRW_7LmwohYrJF9lReT9EamC5nnvZXTwoL6fbjCY1Z0zo8UVy52Rk3_ZQcuGQfj-bGyPYkzkOEt8gg=","summary":[{"type":"summary_text","text":"**Identifying real-process test harness candidates**"}]},{"id":"fc_0b42584a8ab0545d016a58aab5ca388199aee7eb703ea3a406","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading endpoint survival harness\",\"path\":\"crates/spt-daemon/tests/endpoint_survival.rs\",\"selector\":\"1-430\"}","call_id":"call_39zmaBbIZkS1OV8uCR1VyBs1","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca4c8199b6aee68125aefde1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading process survival harness\",\"path\":\"crates/spt/tests/brain_survive.rs\",\"selector\":\"1-420\"}","call_id":"call_0pkOtncfL8vt3F72R8moAHbC","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca508199a831e9f7e4505205","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain split E2E\",\"path\":\"crates/spt/tests/brain_split.rs\",\"selector\":\"1-430\"}","call_id":"call_FLKOO7dRUl2x1Fa8BNf7Ok6F","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca58819981fd48cd8fd9ea13","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon lifecycle restart test\",\"path\":\"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\",\"selector\":\"1-420\"}","call_id":"call_7NrzgtQlMN48AzK0cRvJhWip","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca5c81998f452ad0b22ac19e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon E2E flow\",\"path\":\"crates/spt-daemon/tests/daemon_e2e.rs\",\"selector\":\"1-360\"}","call_id":"call_TAkuO7g8hA9ixaX7htYFZZSH","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca608199bd1fce33d3ff03b4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach restart flow\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"740-880,1030-1405\"}","call_id":"call_bnIkvsbGFtZKi5CCAPc7IHcv","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca688199a5dc09e9e3146079","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh root cause history\",\"path\":\"docs/UPDATE-WEDGE-ROOT-CAUSE.md\",\"selector\":\"1-320\"}","call_id":"call_ELEW6U5iSUDbETefjzpxChMW","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aab5ca6c819986ab852ec3b3188d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding refresh stall history\",\"pattern\":\"refresh|update apply|spt update|detach|reattach|rc|stall|most.*PTY|session PTY|controlled session\",\"path\":\"docs/UPDATE-WEDGE-ROOT-CAUSE.md;docs/KNOWN-HAZARDS.md;docs/FAULT-MATRIX.md;docs/adr/0018-broker-brain-process-isolation-restoration.md;crates/spt/tests;crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_qsT7irDnwTv10LOqNTaE2H2T","name":"grep"}]},"duration":14134.756699999794,"ttft":4063.0061999997124,"contextSnapshot":{"promptTokens":74831,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_39zmaBbIZkS1OV8uCR1VyBs1|fc_0b42584a8ab0545d016a58aab5ca388199aee7eb703ea3a406","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"crates/spt-daemon/tests/endpoint_survival.rs"},"intent":"Reading endpoint survival harness"},"id":"d44218de","parentId":"34a07b2d","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0pkOtncfL8vt3F72R8moAHbC|fc_0b42584a8ab0545d016a58aab5ca4c8199b6aee68125aefde1","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"crates/spt/tests/brain_survive.rs"},"intent":"Reading process survival harness"},"id":"288a1f60","parentId":"d44218de","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FLKOO7dRUl2x1Fa8BNf7Ok6F|fc_0b42584a8ab0545d016a58aab5ca508199a831e9f7e4505205","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"crates/spt/tests/brain_split.rs"},"intent":"Reading brain split E2E"},"id":"ec7d9409","parentId":"288a1f60","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7NrzgtQlMN48AzK0cRvJhWip|fc_0b42584a8ab0545d016a58aab5ca58819981fd48cd8fd9ea13","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs"},"intent":"Reading daemon lifecycle restart test"},"id":"0d767a9f","parentId":"ec7d9409","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TAkuO7g8hA9ixaX7htYFZZSH|fc_0b42584a8ab0545d016a58aab5ca5c81998f452ad0b22ac19e","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"crates/spt-daemon/tests/daemon_e2e.rs"},"intent":"Reading daemon E2E flow"},"id":"f161b86b","parentId":"0d767a9f","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bnIkvsbGFtZKi5CCAPc7IHcv|fc_0b42584a8ab0545d016a58aab5ca608199bd1fce33d3ff03b4","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading attach restart flow"},"id":"9df0ecee","parentId":"f161b86b","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ELEW6U5iSUDbETefjzpxChMW|fc_0b42584a8ab0545d016a58aab5ca688199a5dc09e9e3146079","toolName":"read","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"docs/UPDATE-WEDGE-ROOT-CAUSE.md"},"intent":"Reading refresh root cause history"},"id":"967d4803","parentId":"9df0ecee","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qsT7irDnwTv10LOqNTaE2H2T|fc_0b42584a8ab0545d016a58aab5ca6c819986ab852ec3b3188d","toolName":"grep","startedAt":"2026-07-16T09:56:05.875Z","args":{"path":"docs/UPDATE-WEDGE-ROOT-CAUSE.md;docs/KNOWN-HAZARDS.md;docs/FAULT-MATRIX.md;docs/adr/0018-broker-brain-process-isolation-restoration.md;crates/spt/tests;crates/spt-daemon/tests"},"intent":"Finding refresh stall history"},"id":"5962c118","parentId":"967d4803","timestamp":"2026-07-16T09:56:05.875Z"}
{"type":"message","id":"56a86876","parentId":"5962c118","timestamp":"2026-07-16T09:56:05.890Z","message":{"role":"toolResult","toolCallId":"call_bnIkvsbGFtZKi5CCAPc7IHcv|fc_0b42584a8ab0545d016a58aab5ca608199bd1fce33d3ff03b4","toolName":"read","content":[{"type":"text","text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\"/c\".to_string(), \"cd & findstr \\\".\\\"\".to_string()],\n    );\n\n    let sid = brain\n        .spawn_session(SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: String::new(),\n            cwd: Some(proj.to_string_lossy().into_owned()),\n            env: Default::default(),\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .expect(\"spawn cwd-printing child in the project cwd\");\n\n    brain.attach(sid, 0).expect(\"attach to read output\");\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…\n}\n…\nfn attach_registers_remote_drive_detection() {\n…\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\n// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n\nuse 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…\n}"}],"details":{"displayContent":{"text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\"/c\".to_string(), \"cd & findstr \\\".\\\"\".to_string()],\n    );\n\n    let sid = brain\n        .spawn_session(SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: String::new(),\n            cwd: Some(proj.to_string_lossy().into_owned()),\n            env: Default::default(),\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .expect(\"spawn cwd-printing child in the project cwd\");\n\n    brain.attach(sid, 0).expect(\"attach to read output\");\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…\n}\n…\nfn attach_registers_remote_drive_detection() {\n…\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\n// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n\nuse 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…\n}","startLine":721,"lineNumbers":[721,null,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,null,923,null,931,null,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,null,1459]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\attach.rs"}}},"isError":false,"timestamp":1784195765890}}
{"type":"message","id":"18ae98ca","parentId":"56a86876","timestamp":"2026-07-16T09:56:05.897Z","message":{"role":"toolResult","toolCallId":"call_ELEW6U5iSUDbETefjzpxChMW|fc_0b42584a8ab0545d016a58aab5ca688199a5dc09e9e3146079","toolName":"read","content":[{"type":"text","text":"Path 'docs/UPDATE-WEDGE-ROOT-CAUSE.md' not found"}],"details":{},"isError":true,"timestamp":1784195765897}}
{"type":"message","id":"3bc3982c","parentId":"18ae98ca","timestamp":"2026-07-16T09:56:05.900Z","message":{"role":"toolResult","toolCallId":"call_39zmaBbIZkS1OV8uCR1VyBs1|fc_0b42584a8ab0545d016a58aab5ca388199aee7eb703ea3a406","toolName":"read","content":[{"type":"text","text":"//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n//! survival, against a REAL broker over the real local IPC.\n//!\n//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n//!\n//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\n//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\n//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n//! endpoint the restart orphaned, resuming from its last ledger session.\n//!\n//! ## What this rig proves\n//!\n//! A fresh broker (an empty session table — exactly the state after a `daemon stop`)\n//! plus a seeded previously-ONLINE, controllable (spt-hosted), relay-less, dead-custody\n//! endpoint with a ledger row → the pass RE-SPAWNS its harness into a live broker PTY\n//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n//!\n//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n//!\n//! Owns the process-global `SPT_HOME` and binds the WELL-KNOWN `broker_socket_name()`\n//! (the pass's session-query + harness-launch both target it). Single `#[test]`.\n\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brain::now_ms;\nuse spt_daemon::{Brain, Broker};\n\n/// A long-lived, quiet harness the `[session.self]` resume spawns — it stays running\n/// (so the broker session persists for the assertion) and produces no output.\n#[cfg(windows)]\nconst HARNESS_CMD: &str = \"cmd /c ping -n 600 127.0.0.1 >nul\";\n#[cfg(unix)]\nconst HARNESS_CMD: &str = \"sleep 600\";\n\n/// Register a live-capable adapter whose `[session.self]` (the is_resume fallback when\n/// no `[session.resume]`) spawns the long-lived quiet harness, so\n/// `launch_harness_brokered_in` has a real program to run.\nfn register_resume_adapter(name: &str) {\n    let toml = format!(\n        \"[adapter]\\nname=\\\"{name}\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n         min_spt_core_version=\\\"0\\\"\\n\\n\\\n         [session.self]\\ncommand='{HARNESS_CMD}'\\nkeys=[]\\n\"\n    );\n    let src = spt_store::perch::spt_home().join(\"srcs\").join(name);\n    std::fs::create_dir_all(&src).unwrap();\n    std::fs::write(src.join(\"manifest.toml\"), toml).unwrap();\n    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &src, 1000)\n        .expect(\"register the resume adapter\");\n}\n\n/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n/// ledger row to resume from — the daemon-restart-orphaned shape.\nfn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\n    use spt_store::perch::{resolve_perch_path, resolve_ready_file, ParentHint};\n    let perch = resolve_perch_path(id, ParentHint::Infer);\n    std::fs::create_dir_all(&perch).unwrap();\n    let mut rec = spt_store::info::InfoJson::new(id, \"0\", std::process::id(), \"sid-prev\", \"live_agent\");\n    rec.adapter = Some(adapter.to_string());\n    // controllable == Some(true): the doyle-Q1 spt-hosted discriminator (broker-PTY),\n    // NOT a user-terminal (Some(false)) or legacy (None) record.\n    rec.controllable = Some(true);\n    spt_store::info::write_info(&perch, &rec).unwrap();\n    // A real spt-hosted perch is online = daemon status + the ready marker.\n    spt_store::info::set_status(&perch, \"online\").unwrap();\n    std::fs::write(resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n    // The ledger row the resume restores (session + adapter + cwd).\n    let entry = spt_store::sessions::SessionEntry {\n        ts: \"0\".to_string(),\n        session_id: \"prev-session\".to_string(),\n        trigger: spt_store::sessions::SessionTrigger::Boot,\n        cwd: None,\n        ordinal: None,\n        adapter: Some(adapter.to_string()),\n    };\n    spt_store::sessions::append(&perch, &entry).unwrap();\n    // NO resume.pid file → custody pid is not alive → the dup guard permits the re-run.\n}\n\n// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n#[test]\nfn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n    let home = tempfile::TempDir::new().expect(\"temp home\");\n    std::env::set_var(\"SPT_HOME\", home.path());\n\n    // A FRESH broker at the well-known name — the empty-session state right after a\n    // `daemon stop`+`start`. The pass's session-query + harness-launch both target it.\n    let name = spt_daemon::broker_socket_name();\n    let broker = Broker::bind(&name).expect(\"bind broker at the well-known name\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    // Let the broker's accept loop come up.\n    for _ in 0..200 {\n        if Brain::cold_start(&name, now_ms()).is_ok() {\n            break;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n\n    let adapter = \"mockresume\";\n    register_resume_adapter(adapter);\n    seed_orphaned_online_endpoint(\"survivor\", adapter);\n\n    // Precondition: the fresh broker hosts NO session for the endpoint (RED state).\n    let sessions_before = live_session_ids(&name);\n    assert!(\n        !sessions_before.contains(&\"survivor\".to_string()),\n        \"precondition: the fresh broker has no session for the orphaned endpoint\"\n    );\n\n    // The pass under test — the one-shot daemon-restart survival re-run.\n    let owlery = spt_store::perch::owlery_dir();\n    let adapters_dir = spt_store::perch::adapters_dir();\n    let registered = spt_runtime::registry::registered(&adapters_dir);\n    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n\n    // The endpoint's harness was re-run → a live broker session now exists for it.\n    let mut appeared = false;\n    let deadline = Instant::now() + Duration::from_secs(10);\n    while Instant::now() < deadline {\n        if live_session_ids(&name).contains(&\"survivor\".to_string()) {\n            appeared = true;\n            break;\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n\n    // Best-effort teardown: kill the long-lived harness child before asserting.\n    if let Some(sid) = survivor_session_id(&name) {\n        if let Some(pid) = broker.session_pid(sid) {\n            kill_pid(pid);\n        }\n    }\n\n    assert!(\n        appeared,\n        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\n         (a live broker session appears for it). RED without the pass: no session.\"\n    );\n}\n\n/// The endpoint ids the broker currently hosts a PTY session for (via a probe brain).\nfn live_session_ids(name: &str) -> Vec<String> {\n    let mut brain = match Brain::cold_start(name, now_ms()) {\n        Ok(b) => b,\n        Err(_) => return Vec::new(),\n    };\n    brain\n        .sessions()\n        .map(|r| r.sessions.into_iter().map(|s| s.endpoint).collect())\n        .unwrap_or_default()\n}\n\n/// The broker session id hosting the \"survivor\" endpoint, if any (for teardown).\nfn survivor_session_id(name: &str) -> Option<u64> {\n    let mut brain = Brain::cold_start(name, now_ms()).ok()?;\n    brain\n        .sessions()\n        .ok()?\n        .sessions\n        .into_iter()\n        .find(|s| s.endpoint == \"survivor\")\n        .map(|s| s.session_id)\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"}],"details":{"displayContent":{"text":"//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\r\n//! survival, against a REAL broker over the real local IPC.\r\n//!\r\n//! ## The bug this gate locks down (seed #6, the field-accept blocker)\r\n//!\r\n//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\r\n//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\r\n//! though their records survived (info.json status + adapter + cwd + a ledger row). The\r\n//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\r\n//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\r\n//! endpoint the restart orphaned, resuming from its last ledger session.\r\n//!\r\n//! ## What this rig proves\r\n//!\r\n//! A fresh broker (an empty session table — exactly the state after a `daemon stop`)\r\n//! plus a seeded previously-ONLINE, controllable (spt-hosted), relay-less, dead-custody\r\n//! endpoint with a ledger row → the pass RE-SPAWNS its harness into a live broker PTY\r\n//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\r\n//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\r\n//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\r\n//!\r\n//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\r\n//!\r\n//! Owns the process-global `SPT_HOME` and binds the WELL-KNOWN `broker_socket_name()`\r\n//! (the pass's session-query + harness-launch both target it). Single `#[test]`.\r\n\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::brain::now_ms;\r\nuse spt_daemon::{Brain, Broker};\r\n\r\n/// A long-lived, quiet harness the `[session.self]` resume spawns — it stays running\r\n/// (so the broker session persists for the assertion) and produces no output.\r\n#[cfg(windows)]\r\nconst HARNESS_CMD: &str = \"cmd /c ping -n 600 127.0.0.1 >nul\";\r\n#[cfg(unix)]\r\nconst HARNESS_CMD: &str = \"sleep 600\";\r\n\r\n/// Register a live-capable adapter whose `[session.self]` (the is_resume fallback when\r\n/// no `[session.resume]`) spawns the long-lived quiet harness, so\r\n/// `launch_harness_brokered_in` has a real program to run.\r\nfn register_resume_adapter(name: &str) {\r\n    let toml = format!(\r\n        \"[adapter]\\nname=\\\"{name}\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\r\n         min_spt_core_version=\\\"0\\\"\\n\\n\\\r\n         [session.self]\\ncommand='{HARNESS_CMD}'\\nkeys=[]\\n\"\r\n    );\r\n    let src = spt_store::perch::spt_home().join(\"srcs\").join(name);\r\n    std::fs::create_dir_all(&src).unwrap();\r\n    std::fs::write(src.join(\"manifest.toml\"), toml).unwrap();\r\n    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &src, 1000)\r\n        .expect(\"register the resume adapter\");\r\n}\r\n\r\n/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\r\n/// ledger row to resume from — the daemon-restart-orphaned shape.\r\nfn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\r\n    use spt_store::perch::{resolve_perch_path, resolve_ready_file, ParentHint};\r\n    let perch = resolve_perch_path(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(&perch).unwrap();\r\n    let mut rec = spt_store::info::InfoJson::new(id, \"0\", std::process::id(), \"sid-prev\", \"live_agent\");\r\n    rec.adapter = Some(adapter.to_string());\r\n    // controllable == Some(true): the doyle-Q1 spt-hosted discriminator (broker-PTY),\r\n    // NOT a user-terminal (Some(false)) or legacy (None) record.\r\n    rec.controllable = Some(true);\r\n    spt_store::info::write_info(&perch, &rec).unwrap();\r\n    // A real spt-hosted perch is online = daemon status + the ready marker.\r\n    spt_store::info::set_status(&perch, \"online\").unwrap();\r\n    std::fs::write(resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\r\n    // The ledger row the resume restores (session + adapter + cwd).\r\n    let entry = spt_store::sessions::SessionEntry {\r\n        ts: \"0\".to_string(),\r\n        session_id: \"prev-session\".to_string(),\r\n        trigger: spt_store::sessions::SessionTrigger::Boot,\r\n        cwd: None,\r\n        ordinal: None,\r\n        adapter: Some(adapter.to_string()),\r\n    };\r\n    spt_store::sessions::append(&perch, &entry).unwrap();\r\n    // NO resume.pid file → custody pid is not alive → the dup guard permits the re-run.\r\n}\r\n\r\n// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n#[test]\r\nfn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\r\n    let home = tempfile::TempDir::new().expect(\"temp home\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n\r\n    // A FRESH broker at the well-known name — the empty-session state right after a\r\n    // `daemon stop`+`start`. The pass's session-query + harness-launch both target it.\r\n    let name = spt_daemon::broker_socket_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker at the well-known name\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n    // Let the broker's accept loop come up.\r\n    for _ in 0..200 {\r\n        if Brain::cold_start(&name, now_ms()).is_ok() {\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n\r\n    let adapter = \"mockresume\";\r\n    register_resume_adapter(adapter);\r\n    seed_orphaned_online_endpoint(\"survivor\", adapter);\r\n\r\n    // Precondition: the fresh broker hosts NO session for the endpoint (RED state).\r\n    let sessions_before = live_session_ids(&name);\r\n    assert!(\r\n        !sessions_before.contains(&\"survivor\".to_string()),\r\n        \"precondition: the fresh broker has no session for the orphaned endpoint\"\r\n    );\r\n\r\n    // The pass under test — the one-shot daemon-restart survival re-run.\r\n    let owlery = spt_store::perch::owlery_dir();\r\n    let adapters_dir = spt_store::perch::adapters_dir();\r\n    let registered = spt_runtime::registry::registered(&adapters_dir);\r\n    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n\r\n    // The endpoint's harness was re-run → a live broker session now exists for it.\r\n    let mut appeared = false;\r\n    let deadline = Instant::now() + Duration::from_secs(10);\r\n    while Instant::now() < deadline {\r\n        if live_session_ids(&name).contains(&\"survivor\".to_string()) {\r\n            appeared = true;\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n\r\n    // Best-effort teardown: kill the long-lived harness child before asserting.\r\n    if let Some(sid) = survivor_session_id(&name) {\r\n        if let Some(pid) = broker.session_pid(sid) {\r\n            kill_pid(pid);\r\n        }\r\n    }\r\n\r\n    assert!(\r\n        appeared,\r\n        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\r\n         (a live broker session appears for it). RED without the pass: no session.\"\r\n    );\r\n}\r\n\r\n/// The endpoint ids the broker currently hosts a PTY session for (via a probe brain).\r\nfn live_session_ids(name: &str) -> Vec<String> {\r\n    let mut brain = match Brain::cold_start(name, now_ms()) {\r\n        Ok(b) => b,\r\n        Err(_) => return Vec::new(),\r\n    };\r\n    brain\r\n        .sessions()\r\n        .map(|r| r.sessions.into_iter().map(|s| s.endpoint).collect())\r\n        .unwrap_or_default()\r\n}\r\n\r\n/// The broker session id hosting the \"survivor\" endpoint, if any (for teardown).\r\nfn survivor_session_id(name: &str) -> Option<u64> {\r\n    let mut brain = Brain::cold_start(name, now_ms()).ok()?;\r\n    brain\r\n        .sessions()\r\n        .ok()?\r\n        .sessions\r\n        .into_iter()\r\n        .find(|s| s.endpoint == \"survivor\")\r\n        .map(|s| s.session_id)\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","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\endpoint_survival.rs"}}},"isError":false,"timestamp":1784195765900}}
{"type":"message","id":"a5d68948","parentId":"3bc3982c","timestamp":"2026-07-16T09:56:05.900Z","message":{"role":"toolResult","toolCallId":"call_7NrzgtQlMN48AzK0cRvJhWip|fc_0b42584a8ab0545d016a58aab5ca58819981fd48cd8fd9ea13","toolName":"read","content":[{"type":"text","text":"//! M11-W0.4 (i) production-path int (REQ-DAEMON-1) — re-points the\n//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which\n//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED\n//! production path, the M3-era false-green) ONTO the actual brain entry point:\n//!\n//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,\n//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS a real ONLINE live\n//! endpoint and DRIVES its pulse loop — proven by the loop ingesting a commune into\n//! the live-context tier. The reconcile that hosts is the REAL brain's, not an in-\n//! test `reconcile_once` call. A cold `run_brain` over an already-online perch IS\n//! the post-brain-restart rehost: the perch's `status=online` survives the brain\n//! process, and the fresh brain re-hosts the lifecycle at boot (daemon-authoritative\n//! liveness, CONTEXT:194).\n//!\n//! **F-030 W1:** the Psyche is now a PER-EVENT TURN, not a resident process, so this\n//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\n//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\n//! ingesting the commune — unchanged and independent of the retired resident spawn.\n//!\n//! Single `#[test]`: it owns the process-global `SPT_HOME`.\n// [int->REQ-DAEMON-1]\n\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brainproc::{run_brain, StartReason};\nuse spt_daemon::endpoint::broker_socket_name;\nuse spt_daemon::Broker;\nuse spt_store::liveness::STATUS_ONLINE;\nuse spt_store::perch::{self, ParentHint};\n\n// A no-op `psyche_init` command. F-030 W1 host_one spawns NO resident psyche, but\n// `BrainLifecycle::with_config_in` still REQUIRES the manifest to declare the role,\n// so it stays present (never spawned). The pulse loop drives regardless — commune\n// ingest is independent of any psyche process.\n#[cfg(windows)]\nconst NOOP_PSYCHE: &str = \"cmd /C rem\";\n#[cfg(unix)]\nconst NOOP_PSYCHE: &str = \"true\";\n\n#[test]\nfn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {\n    let home = tempfile::tempdir().unwrap();\n    std::env::set_var(\"SPT_HOME\", home.path());\n\n    // A live-capable adapter, registered so the brain resolves the manifest from\n    // disk by the perch's adapter (the production resolve path).\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 = format!(\n        \"[adapter]\\nname=\\\"mock\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n         min_spt_core_version=\\\"0\\\"\\n\\n\\\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\n         [session.psyche_init]\\ncommand='{NOOP_PSYCHE}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\n    );\n    let src = perch::spt_home().join(\"srcs\").join(\"mock\");\n    std::fs::create_dir_all(&src).unwrap();\n    std::fs::write(src.join(\"manifest.toml\"), &manifest).unwrap();\n    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n\n    // An ONLINE live endpoint on disk — as a real `api bind/listen` left it (the\n    // establish→online path is proven by live_firsthost/live_bind ints; here the\n    // focus is the REAL brain hosting it). status=online survives the brain.\n    let id = \"agent9\";\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n    std::fs::create_dir_all(&perch_path).unwrap();\n    let mut rec = spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-9\", \"live_agent\");\n    rec.adapter = Some(\"mock\".to_string());\n    spt_store::info::write_info(&perch_path, &rec).unwrap();\n    spt_store::info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n\n    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct\n    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile\n    // hosting the Psyche is the real brain's, at boot.\n    let broker = Broker::bind(&broker_socket_name()).expect(\"bind broker\");\n    {\n        let serve = Arc::clone(&broker);\n        thread::spawn(move || {\n            let _ = serve.serve();\n        });\n    }\n    thread::spawn(|| {\n        let _ = run_brain(0, StartReason::Cold);\n    });\n\n    // The real brain's pulse loop runs (F-030 W1: NO resident psyche is spawned —\n    // the DAEMON-1 evidence is the hosted loop ticking, not a bound `<id>-psyche`\n    // perch): a commune drop is ingested into the live context tier (proves the loop\n    // ticks in the brain, not a test-driven tick).\n    std::fs::write(\n        drops.join(format!(\"{id}-commune.md\")),\n        \"Source: self-commune\\nreal-brain note\",\n    )\n    .unwrap();\n    let ctx = spt_store::contextstore::live_context_file(&home.path().join(\"tracked\"), id);\n    let deadline = Instant::now() + Duration::from_secs(25);\n    loop {\n        if std::fs::read_to_string(&ctx)\n            .map(|s| s.contains(\"real-brain note\"))\n            .unwrap_or(false)\n        {\n            break;\n        }\n        assert!(\n            Instant::now() < deadline,\n            \"the real brain's pulse loop must ingest the commune into the live context tier\"\n        );\n        thread::sleep(Duration::from_millis(100));\n    }\n\n    // No resident psyche to reap (F-030 W1 — host_one spawns none). The run_brain +\n    // serve threads are detached and die with the test process.\n}\n"}],"details":{"displayContent":{"text":"//! M11-W0.4 (i) production-path int (REQ-DAEMON-1) — re-points the\r\n//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which\r\n//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED\r\n//! production path, the M3-era false-green) ONTO the actual brain entry point:\r\n//!\r\n//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,\r\n//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS a real ONLINE live\r\n//! endpoint and DRIVES its pulse loop — proven by the loop ingesting a commune into\r\n//! the live-context tier. The reconcile that hosts is the REAL brain's, not an in-\r\n//! test `reconcile_once` call. A cold `run_brain` over an already-online perch IS\r\n//! the post-brain-restart rehost: the perch's `status=online` survives the brain\r\n//! process, and the fresh brain re-hosts the lifecycle at boot (daemon-authoritative\r\n//! liveness, CONTEXT:194).\r\n//!\r\n//! **F-030 W1:** the Psyche is now a PER-EVENT TURN, not a resident process, so this\r\n//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\r\n//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\r\n//! ingesting the commune — unchanged and independent of the retired resident spawn.\r\n//!\r\n//! Single `#[test]`: it owns the process-global `SPT_HOME`.\r\n// [int->REQ-DAEMON-1]\r\n\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::brainproc::{run_brain, StartReason};\r\nuse spt_daemon::endpoint::broker_socket_name;\r\nuse spt_daemon::Broker;\r\nuse spt_store::liveness::STATUS_ONLINE;\r\nuse spt_store::perch::{self, ParentHint};\r\n\r\n// A no-op `psyche_init` command. F-030 W1 host_one spawns NO resident psyche, but\r\n// `BrainLifecycle::with_config_in` still REQUIRES the manifest to declare the role,\r\n// so it stays present (never spawned). The pulse loop drives regardless — commune\r\n// ingest is independent of any psyche process.\r\n#[cfg(windows)]\r\nconst NOOP_PSYCHE: &str = \"cmd /C rem\";\r\n#[cfg(unix)]\r\nconst NOOP_PSYCHE: &str = \"true\";\r\n\r\n#[test]\r\nfn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {\r\n    let home = tempfile::tempdir().unwrap();\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n\r\n    // A live-capable adapter, registered so the brain resolves the manifest from\r\n    // disk by the perch's adapter (the production resolve path).\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 = format!(\r\n        \"[adapter]\\nname=\\\"mock\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\r\n         min_spt_core_version=\\\"0\\\"\\n\\n\\\r\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\r\n         [session.psyche_init]\\ncommand='{NOOP_PSYCHE}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\r\n    );\r\n    let src = perch::spt_home().join(\"srcs\").join(\"mock\");\r\n    std::fs::create_dir_all(&src).unwrap();\r\n    std::fs::write(src.join(\"manifest.toml\"), &manifest).unwrap();\r\n    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\r\n\r\n    // An ONLINE live endpoint on disk — as a real `api bind/listen` left it (the\r\n    // establish→online path is proven by live_firsthost/live_bind ints; here the\r\n    // focus is the REAL brain hosting it). status=online survives the brain.\r\n    let id = \"agent9\";\r\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(&perch_path).unwrap();\r\n    let mut rec = spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-9\", \"live_agent\");\r\n    rec.adapter = Some(\"mock\".to_string());\r\n    spt_store::info::write_info(&perch_path, &rec).unwrap();\r\n    spt_store::info::set_status(&perch_path, STATUS_ONLINE).unwrap();\r\n\r\n    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct\r\n    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile\r\n    // hosting the Psyche is the real brain's, at boot.\r\n    let broker = Broker::bind(&broker_socket_name()).expect(\"bind broker\");\r\n    {\r\n        let serve = Arc::clone(&broker);\r\n        thread::spawn(move || {\r\n            let _ = serve.serve();\r\n        });\r\n    }\r\n    thread::spawn(|| {\r\n        let _ = run_brain(0, StartReason::Cold);\r\n    });\r\n\r\n    // The real brain's pulse loop runs (F-030 W1: NO resident psyche is spawned —\r\n    // the DAEMON-1 evidence is the hosted loop ticking, not a bound `<id>-psyche`\r\n    // perch): a commune drop is ingested into the live context tier (proves the loop\r\n    // ticks in the brain, not a test-driven tick).\r\n    std::fs::write(\r\n        drops.join(format!(\"{id}-commune.md\")),\r\n        \"Source: self-commune\\nreal-brain note\",\r\n    )\r\n    .unwrap();\r\n    let ctx = spt_store::contextstore::live_context_file(&home.path().join(\"tracked\"), id);\r\n    let deadline = Instant::now() + Duration::from_secs(25);\r\n    loop {\r\n        if std::fs::read_to_string(&ctx)\r\n            .map(|s| s.contains(\"real-brain note\"))\r\n            .unwrap_or(false)\r\n        {\r\n            break;\r\n        }\r\n        assert!(\r\n            Instant::now() < deadline,\r\n            \"the real brain's pulse loop must ingest the commune into the live context tier\"\r\n        );\r\n        thread::sleep(Duration::from_millis(100));\r\n    }\r\n\r\n    // No resident psyche to reap (F-030 W1 — host_one spawns none). The run_brain +\r\n    // serve threads are detached and die with the test process.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\daemon_lifecycle_real_brain.rs"}}},"isError":false,"timestamp":1784195765900}}
{"type":"message","id":"c8e2026d","parentId":"a5d68948","timestamp":"2026-07-16T09:56:05.901Z","message":{"role":"toolResult","toolCallId":"call_TAkuO7g8hA9ixaX7htYFZZSH|fc_0b42584a8ab0545d016a58aab5ca5c81998f452ad0b22ac19e","toolName":"read","content":[{"type":"text","text":"//! B9 daemon end-to-end — the M3b milestone proof, driving the whole consolidated\n//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's\n//! lifecycle as in-process loops **and** survives a brain restart with its hosted\n//! PTY child + output stream intact and gapless.\n//!\n//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):\n//!\n//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,\n//!    spawns the session, and round-trips a marker through it (the session is live).\n//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\n//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\n//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\n//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\n//! 3. **commune** — a commune drop lands in the watched dir and a scheduled pulse\n//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).\n//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off\n//!    repeatedly; across each handoff the child's pid is unchanged, exactly one\n//!    session is hosted, the output stream replays gaplessly, and `gen_start`\n//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is\n//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n//!    marker ([`REQ-DAEMON-4`]).\n//!\n//! The child is the same cross-OS echo program the other daemon tests use (`cat` /\n//! `findstr \".\"`); lines END IN `\\r` (the Enter key) so the real PTY submits +\n//! echoes them, exactly as B8 documents (bare `\\n` echoes a lone `\\r` and collapses\n//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).\n//!\n//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it\n//! must not race a sibling test in this binary.\n// REQ-DAEMON-1 `int` re-pointed to crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\n// (M11-W0.4 (i)): the REAL brain PROCESS (run_brain) hosts the Psyche, retiring\n// this file's type-level BrainLifecycle::with_config-direct over-claim (it kept\n// green coverage over an UNWIRED production path — the M3-era false-green). This\n// file keeps its REQ-DAEMON-4 + hazard int evidence (the lifecycle machinery +\n// daemon-hosted-liveness), which it still exercises directly.\n// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n// brain-restart survival, not the process-level swap. Process-level endpoint\n// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n// [int->REQ-DAEMON-4]\n// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_daemon::brain::BrainState;\nuse spt_daemon::msg::SpawnReq;\nuse spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};\nuse spt_runtime::Manifest;\nuse spt_store::perch::{self, ParentHint};\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique(prefix: &str) -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-b9-{prefix}-{}-{}.sock\", std::process::id(), n)\n}\n\n/// The cross-OS echo child: reads stdin, writes each line back to stdout.\nfn echo_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// A no-op Psyche summarizer command (we never depend on its output here; spawning\n// it exercises the hosted-Psyche bind + the daemon online-stamp).\n#[cfg(windows)]\nconst NOOP_CMD: &str = \"cmd /C rem\";\n#[cfg(unix)]\nconst NOOP_CMD: &str = \"true\";\n\n// A stdin→stdout passthrough so the echo-commune summarizer writes a real drop\n// file — lets the signoff assert the echo (3.3) actually ran before teardown.\n#[cfg(windows)]\nconst PASSTHROUGH: &str = \"sort\";\n#[cfg(unix)]\nconst PASSTHROUGH: &str = \"cat\";\n\n/// A live-capable manifest declaring a Psyche, a watched commune/signoff dir, and\n/// an echo-commune summarizer (so `fire_echo` writes a real commune on signoff).\nfn live_manifest(drops: &std::path::Path) -> Manifest {\n    let dir = drops.to_string_lossy().replace('\\\\', \"/\");\n    let toml = format!(\n        \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"0.1.0\\\"\\n\\n\\\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\n         [session.psyche_init]\\ncommand='{NOOP_CMD}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\\n\\\n         [session.echo_commune]\\ncommand='{PASSTHROUGH}'\\n\"\n    );\n    Manifest::from_toml_str(&toml).unwrap()\n}\n\nfn cold_retry(name: &str, now_ms: u64) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, now_ms) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain cold-start could not connect\");\n}\n\nfn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain handoff could not connect\");\n}\n\n#[test]\nfn daemon_hosts_lifecycle_and_survives_brain_restart() {\n    // Isolate all on-disk perch state (the Self perch, the nested Psyche perch,\n    // the commune drops) to a temp home.\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    std::env::set_var(\"SPT_HOME\", home.path());\n\n    let id = \"doyle\";\n    let drops = tempfile::tempdir().expect(\"drops dir\");\n    let manifest = live_manifest(drops.path());\n    // A short pulse keeps the test quick; proves the period is config-driven.\n    let cfg = DaemonConfig {\n        pulse_period: Duration::from_millis(15),\n        crash_grace: Duration::from_millis(10),\n        ..DaemonConfig::default()\n    };\n    let host = BrainLifecycle::with_config(&manifest, id, cfg).expect(\"live host\");\n\n    // ── 1. spawn: the broker hosts a real PTY child; a brain drives it ──────────\n    let broker_name = unique(\"broker\");\n    let broker = Broker::bind(&broker_name).expect(\"bind broker\");\n    {\n        let broker = Arc::clone(&broker);\n        thread::spawn(move || {\n            let _ = broker.serve();\n        });\n    }\n    let mut now = 1_000_000u64;\n    let mut brain = cold_retry(&broker_name, now);\n    let sid = brain.spawn_session(echo_req()).expect(\"spawn echo child\");\n    let child_pid = broker.session_pid(sid);\n    assert_eq!(brain.generation(), 0, \"cold start is generation 0\");\n\n    // The session is live: a marker round-trips through the real PTY.\n    let mut buf = Vec::<u8>::new();\n    brain.send_input(b\"HELLO_DAEMON\\r\").expect(\"inject\");\n    brain\n        .read_output_until(b\"HELLO_DAEMON\", &mut buf)\n        .expect(\"marker round-trips\");\n\n    // ── 2. Psyche-loop: the daemon hosts the Psyche; liveness is the status ─────\n    use spt_store::info::{read_info, set_status, write_info, InfoJson, PidValue};\n    use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};\n\n    // F-030: the resident `spawn_psyche` is retired — a psyche is a bounded per-event\n    // turn. This leg proves DAEMON-HOSTED LIVENESS (status-authoritative, not pid), so it\n    // binds a daemon-hosted psyche perch DIRECTLY (create the nested dir + write its info\n    // record + stamp it ONLINE, exactly the online-stamp a daemon host applies) instead of\n    // spawning a resident child.\n    let psyche_perch = perch::resolve_perch_path(\"doyle-psyche\", ParentHint::Explicit(id));\n    std::fs::create_dir_all(&psyche_perch).unwrap();\n    write_info(\n        &psyche_perch,\n        &InfoJson::new(\"doyle-psyche\", \"t\", std::process::id(), \"sid-1\", \"psyche\"),\n    )\n    .unwrap();\n    set_status(&psyche_perch, STATUS_ONLINE).unwrap();\n    assert_eq!(\n        read_info(&psyche_perch)\n            .expect(\"psyche info\")\n            .status\n            .as_deref(),\n        Some(STATUS_ONLINE),\n        \"the daemon marked the hosted Psyche online\"\n    );\n    // The summarizer subprocess exiting (its recorded pid now dead) must NOT flip\n    // the daemon-hosted perch offline — liveness is the daemon status, not the pid.\n    const DEAD_PID: u32 = 2_000_000_000;\n    let mut rec = read_info(&psyche_perch).unwrap();\n    rec.pid = PidValue::Numeric(DEAD_PID);\n    write_info(&psyche_perch, &rec).unwrap();\n    assert!(\n        is_perch_alive(&psyche_perch),\n        \"a dead summarizer pid must NOT flip a daemon-hosted perch offline\"\n    );\n\n    // ── 3. commune: a drop lands and a pulse tick folds it in ───────────────────\n    std::fs::write(drops.path().join(\"doyle-commune.md\"), \"first brief\").unwrap();\n    let report = host.pulse_tick(Some(\"sid-1\")).expect(\"pulse tick\");\n    assert_eq!(\n        report.ingested.len(),\n        1,\n        \"the commune drop was ingested by the loop\"\n    );\n    assert!(\n        !drops.path().join(\"doyle-commune.md\").exists(),\n        \"drop consumed\"\n    );\n\n    // ── 4. brain-restart-survives: kill+restart the brain repeatedly ────────────\n    const RESTARTS: u64 = 10;\n    // Inject the first successor marker, then crash (drop the connection).\n    brain\n        .send_input(b\"MARKER_1_DONE\\r\")\n        .expect(\"inject first marker\");\n    let mut state = brain.snapshot();\n    drop(brain);\n\n    for k in 1..=RESTARTS {\n        now += 1; // advance wall clock so a fresh gen_start is strictly greater\n        let prior_gen_start = state.gen_start_ms;\n        let prior_generation = state.generation;\n        let mut brain = handoff_retry(&broker_name, state.clone(), now);\n\n        // 2.4 — gen_start is now(), never the rehydrated stale value.\n        assert_eq!(brain.gen_start_ms(), now, \"gen_start must be now()\");\n        assert_ne!(\n            brain.gen_start_ms(),\n            prior_gen_start,\n            \"gen_start not carried forward\"\n        );\n        assert_eq!(\n            brain.generation(),\n            prior_generation + 1,\n            \"generation increments\"\n        );\n\n        // The child + its PTY survived the restart untouched.\n        assert_eq!(\n            broker.session_count(),\n            1,\n            \"exactly one session across restarts\"\n        );\n        assert_eq!(\n            broker.session_pid(sid),\n            child_pid,\n            \"child pid stable across restart\"\n        );\n\n        // Gapless resume: the marker the previous brain injected (and died before\n        // reading) arrives via the replayed buffer.\n        let marker = format!(\"MARKER_{k}_DONE\");\n        brain\n            .read_output_until(marker.as_bytes(), &mut buf)\n            .unwrap_or_else(|e| panic!(\"gapless resume must replay {marker}: {e}\"));\n\n        // The daemon-hosted lifecycle is stateless on disk, so it keeps ingesting\n        // straight across the brain restart: drop a commune, tick, confirm.\n        std::fs::write(drops.path().join(\"doyle-commune.md\"), format!(\"brief {k}\")).unwrap();\n        let rep = host\n            .pulse_tick(Some(\"sid-1\"))\n            .expect(\"pulse tick across restart\");\n        assert_eq!(\n            rep.ingested.len(),\n            1,\n            \"lifecycle keeps ingesting across the brain restart\"\n        );\n\n        // Hand the next marker to our successor, then crash without reading it.\n        if k < RESTARTS {\n            brain\n                .send_input(format!(\"MARKER_{}_DONE\\r\", k + 1).as_bytes())\n                .expect(\"inject next marker\");\n        }\n        state = brain.snapshot();\n        drop(brain);\n    }\n\n    // ── 5. graceful-signoff: echo BEFORE teardown (3.3), ready marker removed ───\n    // Seed the Self's ready marker (graceful teardown removes it).\n    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n    std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n    std::fs::write(&ready, \"\").unwrap();\n\n    let outcome = host\n        .graceful_signoff(Some(\"sid-1\"))\n        .expect(\"graceful signoff\");\n    assert!(outcome.echo_ran, \"echo-before-signoff (3.3) must have run\");\n    assert!(outcome.signed_off, \"graceful signoff tears the Self down\");\n    assert!(\n        drops.path().join(\"doyle-commune.md\").exists(),\n        \"the echo wrote a final-delta commune before teardown\"\n    );\n    assert!(!ready.exists(), \"teardown removed the Self's ready marker\");\n\n    // The hosted Psyche is marked offline on teardown (the online-stamp counterpart).\n    BrainLifecycle::mark_offline(&psyche_perch);\n    assert!(\n        !is_perch_alive(&psyche_perch),\n        \"mark_offline flips the perch offline\"\n    );\n\n    // ── Reap: kill the child and confirm the broker drops the session ───────────\n    now += 1;\n    let mut brain = handoff_retry(&broker_name, state, now);\n    brain.kill_session().expect(\"kill child\");\n    drop(brain);\n    let mut reaped = false;\n    for _ in 0..200 {\n        if broker.session_count() == 0 {\n            reaped = true;\n            break;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    assert!(\n        reaped,\n        \"child exit must reap the session from the broker table\"\n    );\n}\n"}],"details":{"displayContent":{"text":"//! B9 daemon end-to-end — the M3b milestone proof, driving the whole consolidated\r\n//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's\r\n//! lifecycle as in-process loops **and** survives a brain restart with its hosted\r\n//! PTY child + output stream intact and gapless.\r\n//!\r\n//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):\r\n//!\r\n//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,\r\n//!    spawns the session, and round-trips a marker through it (the session is live).\r\n//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\r\n//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\r\n//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\r\n//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\r\n//! 3. **commune** — a commune drop lands in the watched dir and a scheduled pulse\r\n//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).\r\n//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off\r\n//!    repeatedly; across each handoff the child's pid is unchanged, exactly one\r\n//!    session is hosted, the output stream replays gaplessly, and `gen_start`\r\n//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is\r\n//!    stateless on disk, so it keeps ingesting communes straight across the restart.\r\n//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\r\n//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\r\n//!    marker ([`REQ-DAEMON-4`]).\r\n//!\r\n//! The child is the same cross-OS echo program the other daemon tests use (`cat` /\r\n//! `findstr \".\"`); lines END IN `\\r` (the Enter key) so the real PTY submits +\r\n//! echoes them, exactly as B8 documents (bare `\\n` echoes a lone `\\r` and collapses\r\n//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).\r\n//!\r\n//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it\r\n//! must not race a sibling test in this binary.\r\n// REQ-DAEMON-1 `int` re-pointed to crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\r\n// (M11-W0.4 (i)): the REAL brain PROCESS (run_brain) hosts the Psyche, retiring\r\n// this file's type-level BrainLifecycle::with_config-direct over-claim (it kept\r\n// green coverage over an UNWIRED production path — the M3-era false-green). This\r\n// file keeps its REQ-DAEMON-4 + hazard int evidence (the lifecycle machinery +\r\n// daemon-hosted-liveness), which it still exercises directly.\r\n// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\r\n// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\r\n// brain-restart survival, not the process-level swap. Process-level endpoint\r\n// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\r\n// [int->REQ-DAEMON-4]\r\n// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse spt_daemon::brain::BrainState;\r\nuse spt_daemon::msg::SpawnReq;\r\nuse spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};\r\nuse spt_runtime::Manifest;\r\nuse spt_store::perch::{self, ParentHint};\r\n\r\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\r\nfn unique(prefix: &str) -> String {\r\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\r\n    format!(\"spt-daemon-b9-{prefix}-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\n/// The cross-OS echo child: reads stdin, writes each line back to stdout.\r\nfn echo_req() -> 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: String::new(),\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// A no-op Psyche summarizer command (we never depend on its output here; spawning\r\n// it exercises the hosted-Psyche bind + the daemon online-stamp).\r\n#[cfg(windows)]\r\nconst NOOP_CMD: &str = \"cmd /C rem\";\r\n#[cfg(unix)]\r\nconst NOOP_CMD: &str = \"true\";\r\n\r\n// A stdin→stdout passthrough so the echo-commune summarizer writes a real drop\r\n// file — lets the signoff assert the echo (3.3) actually ran before teardown.\r\n#[cfg(windows)]\r\nconst PASSTHROUGH: &str = \"sort\";\r\n#[cfg(unix)]\r\nconst PASSTHROUGH: &str = \"cat\";\r\n\r\n/// A live-capable manifest declaring a Psyche, a watched commune/signoff dir, and\r\n/// an echo-commune summarizer (so `fire_echo` writes a real commune on signoff).\r\nfn live_manifest(drops: &std::path::Path) -> Manifest {\r\n    let dir = drops.to_string_lossy().replace('\\\\', \"/\");\r\n    let toml = format!(\r\n        \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"0.1.0\\\"\\n\\n\\\r\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\r\n         [session.psyche_init]\\ncommand='{NOOP_CMD}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\\n\\\r\n         [session.echo_commune]\\ncommand='{PASSTHROUGH}'\\n\"\r\n    );\r\n    Manifest::from_toml_str(&toml).unwrap()\r\n}\r\n\r\nfn cold_retry(name: &str, now_ms: u64) -> Brain {\r\n    for _ in 0..300 {\r\n        if let Ok(b) = Brain::cold_start(name, now_ms) {\r\n            return b;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"brain cold-start could not connect\");\r\n}\r\n\r\nfn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\r\n    for _ in 0..300 {\r\n        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {\r\n            return b;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"brain handoff could not connect\");\r\n}\r\n\r\n#[test]\r\nfn daemon_hosts_lifecycle_and_survives_brain_restart() {\r\n    // Isolate all on-disk perch state (the Self perch, the nested Psyche perch,\r\n    // the commune drops) to a temp home.\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n\r\n    let id = \"doyle\";\r\n    let drops = tempfile::tempdir().expect(\"drops dir\");\r\n    let manifest = live_manifest(drops.path());\r\n    // A short pulse keeps the test quick; proves the period is config-driven.\r\n    let cfg = DaemonConfig {\r\n        pulse_period: Duration::from_millis(15),\r\n        crash_grace: Duration::from_millis(10),\r\n        ..DaemonConfig::default()\r\n    };\r\n    let host = BrainLifecycle::with_config(&manifest, id, cfg).expect(\"live host\");\r\n\r\n    // ── 1. spawn: the broker hosts a real PTY child; a brain drives it ──────────\r\n    let broker_name = unique(\"broker\");\r\n    let broker = Broker::bind(&broker_name).expect(\"bind broker\");\r\n    {\r\n        let broker = Arc::clone(&broker);\r\n        thread::spawn(move || {\r\n            let _ = broker.serve();\r\n        });\r\n    }\r\n    let mut now = 1_000_000u64;\r\n    let mut brain = cold_retry(&broker_name, now);\r\n    let sid = brain.spawn_session(echo_req()).expect(\"spawn echo child\");\r\n    let child_pid = broker.session_pid(sid);\r\n    assert_eq!(brain.generation(), 0, \"cold start is generation 0\");\r\n\r\n    // The session is live: a marker round-trips through the real PTY.\r\n    let mut buf = Vec::<u8>::new();\r\n    brain.send_input(b\"HELLO_DAEMON\\r\").expect(\"inject\");\r\n    brain\r\n        .read_output_until(b\"HELLO_DAEMON\", &mut buf)\r\n        .expect(\"marker round-trips\");\r\n\r\n    // ── 2. Psyche-loop: the daemon hosts the Psyche; liveness is the status ─────\r\n    use spt_store::info::{read_info, set_status, write_info, InfoJson, PidValue};\r\n    use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};\r\n\r\n    // F-030: the resident `spawn_psyche` is retired — a psyche is a bounded per-event\r\n    // turn. This leg proves DAEMON-HOSTED LIVENESS (status-authoritative, not pid), so it\r\n    // binds a daemon-hosted psyche perch DIRECTLY (create the nested dir + write its info\r\n    // record + stamp it ONLINE, exactly the online-stamp a daemon host applies) instead of\r\n    // spawning a resident child.\r\n    let psyche_perch = perch::resolve_perch_path(\"doyle-psyche\", ParentHint::Explicit(id));\r\n    std::fs::create_dir_all(&psyche_perch).unwrap();\r\n    write_info(\r\n        &psyche_perch,\r\n        &InfoJson::new(\"doyle-psyche\", \"t\", std::process::id(), \"sid-1\", \"psyche\"),\r\n    )\r\n    .unwrap();\r\n    set_status(&psyche_perch, STATUS_ONLINE).unwrap();\r\n    assert_eq!(\r\n        read_info(&psyche_perch)\r\n            .expect(\"psyche info\")\r\n            .status\r\n            .as_deref(),\r\n        Some(STATUS_ONLINE),\r\n        \"the daemon marked the hosted Psyche online\"\r\n    );\r\n    // The summarizer subprocess exiting (its recorded pid now dead) must NOT flip\r\n    // the daemon-hosted perch offline — liveness is the daemon status, not the pid.\r\n    const DEAD_PID: u32 = 2_000_000_000;\r\n    let mut rec = read_info(&psyche_perch).unwrap();\r\n    rec.pid = PidValue::Numeric(DEAD_PID);\r\n    write_info(&psyche_perch, &rec).unwrap();\r\n    assert!(\r\n        is_perch_alive(&psyche_perch),\r\n        \"a dead summarizer pid must NOT flip a daemon-hosted perch offline\"\r\n    );\r\n\r\n    // ── 3. commune: a drop lands and a pulse tick folds it in ───────────────────\r\n    std::fs::write(drops.path().join(\"doyle-commune.md\"), \"first brief\").unwrap();\r\n    let report = host.pulse_tick(Some(\"sid-1\")).expect(\"pulse tick\");\r\n    assert_eq!(\r\n        report.ingested.len(),\r\n        1,\r\n        \"the commune drop was ingested by the loop\"\r\n    );\r\n    assert!(\r\n        !drops.path().join(\"doyle-commune.md\").exists(),\r\n        \"drop consumed\"\r\n    );\r\n\r\n    // ── 4. brain-restart-survives: kill+restart the brain repeatedly ────────────\r\n    const RESTARTS: u64 = 10;\r\n    // Inject the first successor marker, then crash (drop the connection).\r\n    brain\r\n        .send_input(b\"MARKER_1_DONE\\r\")\r\n        .expect(\"inject first marker\");\r\n    let mut state = brain.snapshot();\r\n    drop(brain);\r\n\r\n    for k in 1..=RESTARTS {\r\n        now += 1; // advance wall clock so a fresh gen_start is strictly greater\r\n        let prior_gen_start = state.gen_start_ms;\r\n        let prior_generation = state.generation;\r\n        let mut brain = handoff_retry(&broker_name, state.clone(), now);\r\n\r\n        // 2.4 — gen_start is now(), never the rehydrated stale value.\r\n        assert_eq!(brain.gen_start_ms(), now, \"gen_start must be now()\");\r\n        assert_ne!(\r\n            brain.gen_start_ms(),\r\n            prior_gen_start,\r\n            \"gen_start not carried forward\"\r\n        );\r\n        assert_eq!(\r\n            brain.generation(),\r\n            prior_generation + 1,\r\n            \"generation increments\"\r\n        );\r\n\r\n        // The child + its PTY survived the restart untouched.\r\n        assert_eq!(\r\n            broker.session_count(),\r\n            1,\r\n            \"exactly one session across restarts\"\r\n        );\r\n        assert_eq!(\r\n            broker.session_pid(sid),\r\n            child_pid,\r\n            \"child pid stable across restart\"\r\n        );\r\n\r\n        // Gapless resume: the marker the previous brain injected (and died before\r\n        // reading) arrives via the replayed buffer.\r\n        let marker = format!(\"MARKER_{k}_DONE\");\r\n        brain\r\n            .read_output_until(marker.as_bytes(), &mut buf)\r\n            .unwrap_or_else(|e| panic!(\"gapless resume must replay {marker}: {e}\"));\r\n\r\n        // The daemon-hosted lifecycle is stateless on disk, so it keeps ingesting\r\n        // straight across the brain restart: drop a commune, tick, confirm.\r\n        std::fs::write(drops.path().join(\"doyle-commune.md\"), format!(\"brief {k}\")).unwrap();\r\n        let rep = host\r\n            .pulse_tick(Some(\"sid-1\"))\r\n            .expect(\"pulse tick across restart\");\r\n        assert_eq!(\r\n            rep.ingested.len(),\r\n            1,\r\n            \"lifecycle keeps ingesting across the brain restart\"\r\n        );\r\n\r\n        // Hand the next marker to our successor, then crash without reading it.\r\n        if k < RESTARTS {\r\n            brain\r\n                .send_input(format!(\"MARKER_{}_DONE\\r\", k + 1).as_bytes())\r\n                .expect(\"inject next marker\");\r\n        }\r\n        state = brain.snapshot();\r\n        drop(brain);\r\n    }\r\n\r\n    // ── 5. graceful-signoff: echo BEFORE teardown (3.3), ready marker removed ───\r\n    // Seed the Self's ready marker (graceful teardown removes it).\r\n    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\r\n    std::fs::write(&ready, \"\").unwrap();\r\n\r\n    let outcome = host\r\n        .graceful_signoff(Some(\"sid-1\"))\r\n        .expect(\"graceful signoff\");\r\n    assert!(outcome.echo_ran, \"echo-before-signoff (3.3) must have run\");\r\n    assert!(outcome.signed_off, \"graceful signoff tears the Self down\");\r\n    assert!(\r\n        drops.path().join(\"doyle-commune.md\").exists(),\r\n        \"the echo wrote a final-delta commune before teardown\"\r\n    );\r\n    assert!(!ready.exists(), \"teardown removed the Self's ready marker\");\r\n\r\n    // The hosted Psyche is marked offline on teardown (the online-stamp counterpart).\r\n    BrainLifecycle::mark_offline(&psyche_perch);\r\n    assert!(\r\n        !is_perch_alive(&psyche_perch),\r\n        \"mark_offline flips the perch offline\"\r\n    );\r\n\r\n    // ── Reap: kill the child and confirm the broker drops the session ───────────\r\n    now += 1;\r\n    let mut brain = handoff_retry(&broker_name, state, now);\r\n    brain.kill_session().expect(\"kill child\");\r\n    drop(brain);\r\n    let mut reaped = false;\r\n    for _ in 0..200 {\r\n        if broker.session_count() == 0 {\r\n            reaped = true;\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    assert!(\r\n        reaped,\r\n        \"child exit must reap the session from the broker table\"\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\daemon_e2e.rs"}}},"isError":false,"timestamp":1784195765901}}
{"type":"message","id":"064c5470","parentId":"c8e2026d","timestamp":"2026-07-16T09:56:05.901Z","message":{"role":"toolResult","toolCallId":"call_0pkOtncfL8vt3F72R8moAHbC|fc_0b42584a8ab0545d016a58aab5ca4c8199b6aee68125aefde1","toolName":"read","content":[{"type":"text","text":"//! Restoration D7-1 (ADR-0018 V5) — **process-level survival across a binary\n//! swap**: a hosted PTY child + a live QUIC connection survive a brain-**PROCESS**\n//! restart that lands on a **swapped on-disk binary**, with the broker holding\n//! both endpoints untouched throughout. This is the `int` E2E that re-points the\n//! regression-masked REQ-DAEMON-2 / REQ-UPD-3 evidence off the in-process\n//! `brain_swap.rs` shape onto a real process boundary.\n//!\n//! ## The proof is a TRIANGLE (doyle D7 vet, call 1) — this is leg (b)\n//!\n//! No single test proves the whole claim; three legs do, and this file is honest\n//! only when read alongside the other two:\n//!\n//! - **Leg (a) — the real-broker-process leg: `crates/spt/tests/brain_split.rs`.**\n//!   The REAL `spt daemon run` broker *process* survives a brain kill and respawns\n//!   it. That is where the broker's own process-packaging is proven.\n//! - **Leg (b) — the survival leg: THIS test.** A real brain *child process* +\n//!   a real on-disk binary swap + real broker-**held** endpoints (a PTY child and\n//!   a live QUIC conn). The broker is modeled **in-process** — faithful, not a\n//!   weakening: the broker is the never-restarting anchor *by design*, so an\n//!   in-process [`Broker`] runs the **same Broker code holding the same\n//!   resources** the real broker would across a brain swap. The claim under test\n//!   is the brain process + the swapped binary + broker-held endpoint survival —\n//!   all real here. (Bonus: the in-process broker gives the test direct access to\n//!   its session pid / count and QUIC conn count for the assertions.)\n//! - **Leg (c) — the field leg: D7-4.** The real daemon, a real `apply`, real\n//!   hardware — the `exe_hash` flips and matches the released artifact.\n//!\n//! DEFERRED upgrade: once the live-agent adapter makes daemon-hosted sessions a\n//! production reality, a full-stack single-process E2E (the real `spt daemon run`\n//! hosting *and* surviving) becomes reachable — tracked in `docs/DEFERRED.md`,\n//! NOT D7 work.\n//!\n//! ## What makes this STRONGER than `brain_swap.rs`\n//!\n//! `brain_swap.rs` (REQ-UPD-3's prior `int`) relaunches the brain via an\n//! **in-process closure** — same process image, no disk involved. This test runs\n//! the **real `spt daemon brain` child process** the production\n//! [`supervise_brain`] spawns, swaps the on-disk binary it respawns from, and\n//! proves the post-respawn brain is running the **new bytes** via the D7-1\n//! `exe_hash` breadcrumb (`exe_hash == fixture-B != fixture-A`). It is also the\n//! **first process-level exercise of the D4 cursor-of-record resume with N≥1\n//! hosted sessions** (the supervised child re-attaches the test-established PTY\n//! session on cold start).\n//!\n//! The swap is modeled as a **path flip** between two distinct on-disk fixtures\n//! (B = A + appended padding — PE and ELF both tolerate trailing data, so behavior\n//! is identical but the hash flips). This is faithful to the production swap (the\n//! supervisor respawns from whatever the binary *selection* resolves to) and\n//! sidesteps the Windows running-exe lock (you cannot overwrite a live `.exe`).\n//!\n//! **Held AND functional (doyle F1).** Proving the endpoints are *held* (pid /\n//! session_count / conn_count unchanged) is not proving they are *alive* — a\n//! zombie conn or wedged PTY would pass. So after the swap the test also drives a\n//! marker through the held PTY child and watches the broker's output seq advance:\n//! the full round-trip (input → child → echo → broker ring) works through the\n//! swapped-in brain.\n// [int->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n// [int->REQ-UPD-3]\n// [int->REQ-DAEMON-2]\n\nuse std::path::PathBuf;\nuse std::process::Command;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brain::Brain;\nuse spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};\nuse spt_daemon::codec::write_frame;\nuse spt_daemon::endpoint::broker_socket_name;\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{encode_bytes, InputReq, SpawnReq, KIND_INPUT};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// A long-lived echo child to host under a broker PTY (the endpoint REQ-UPD-3\n/// forbids us from disturbing across the swap). `cat`/`findstr` idle on stdin.\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/// A hermetic net config (mirrors `netbroker.rs`): relays disabled, no discovery,\n/// loopback-only sockets — no real interface touched, no firewall prompt.\nfn hermetic(identity: Identity) -> NetConfig {\n    NetConfig {\n        identity,\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\n/// Bind a broker (serving on a background thread) that owns a hermetic QUIC\n/// endpoint for `identity`, with its effect journal under `dir`.\nfn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n        .expect(\"bind broker with net\");\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..400 {\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/// The three breadcrumb fields out of a `brain.ready` body (parsed inline so this\n/// test depends on no private brainproc helper). `None` until the file exists and\n/// carries a parseable pid.\nfn read_ready(path: &std::path::Path) -> Option<(u32, u64, Option<String>)> {\n    let body = std::fs::read_to_string(path).ok()?;\n    let v: serde_json::Value = serde_json::from_str(&body).ok()?;\n    let pid = v.get(\"pid\")?.as_u64()? as u32;\n    let generation = v.get(\"generation\").and_then(|g| g.as_u64()).unwrap_or(0);\n    let exe_hash = v\n        .get(\"exe_hash\")\n        .and_then(|h| h.as_str())\n        .map(str::to_string);\n    Some((pid, generation, exe_hash))\n}\n\n/// Poll `brain.ready` until it holds a pid (optionally one DIFFERENT from `was`),\n/// returning `(pid, generation, exe_hash)`. `None` if it never appears in time.\nfn wait_ready(\n    path: &std::path::Path,\n    was: Option<u32>,\n    budget: Duration,\n) -> Option<(u32, u64, Option<String>)> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some((pid, gen, hash)) = read_ready(path) {\n            if was != Some(pid) {\n                return Some((pid, gen, hash));\n            }\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// Lowercase-hex SHA-256 of a file (the same digest `brain.ready.exe_hash`\n/// carries; recomputed independently here to assert the breadcrumb matches the\n/// fixture the supervisor actually spawned).\nfn file_hash(path: &std::path::Path) -> String {\n    use sha2::{Digest, Sha256};\n    let bytes = std::fs::read(path).expect(\"read fixture for hashing\");\n    let mut h = Sha256::new();\n    h.update(&bytes);\n    let digest = h.finalize();\n    let mut s = String::with_capacity(digest.len() * 2);\n    for b in digest {\n        s.push_str(&format!(\"{b:02x}\"));\n    }\n    s\n}\n\n#[test]\nfn pty_and_quic_survive_brain_process_restart_onto_swapped_binary() {\n    // The in-process broker + brain children both resolve `broker_socket_name()`\n    // from SPT_HOME, so the supervised child connects to OUR broker. (The\n    // established daemon-test pattern — daemon_e2e.rs et al. set it the same way.)\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    // Single-test binary by design — this is the SOLE `#[test]` here, so it owns\n    // the process-global SPT_HOME on its own thread (cross-binary tests are\n    // separate processes). A SECOND `#[test]` added to this file MUST bring the\n    // env-lock discipline (a shared mutex / serialized home), or it will race this\n    // one — the D6 env-race lesson (see the spt-daemon test_home pattern).\n    std::env::set_var(\"SPT_HOME\", home.path());\n\n    // ── Two fixtures: A, and B = A + appended padding (the on-disk \"swap\"). ──\n    // Both are the real `spt` binary, so `daemon brain` runs; B differs only by\n    // trailing bytes (behavior identical, exe_hash flips).\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let suffix = spt_bin.extension().map(|e| format!(\".{}\", e.to_string_lossy())).unwrap_or_default();\n    let fixture_a = home.path().join(format!(\"brain-A{suffix}\"));\n    let fixture_b = home.path().join(format!(\"brain-B{suffix}\"));\n    std::fs::copy(&spt_bin, &fixture_a).expect(\"stage fixture A\");\n    std::fs::copy(&spt_bin, &fixture_b).expect(\"stage fixture B\");\n    {\n        use std::io::Write;\n        let mut f = std::fs::OpenOptions::new()\n            .append(true)\n            .open(&fixture_b)\n            .expect(\"open fixture B to append padding\");\n        f.write_all(&[0u8; 4096]).expect(\"append padding to fixture B\");\n    }\n    let hash_a = file_hash(&fixture_a);\n    let hash_b = file_hash(&fixture_b);\n    assert_ne!(hash_a, hash_b, \"the swap must flip the binary hash\");\n\n    // ── The broker (leg-b stable kernel) + a loopback QUIC peer. ──\n    let broker = net_broker(&broker_socket_name(), Identity::generate(), &home.path().join(\"a\"));\n    let peer_name = format!(\"spt-d7-peer-{}.sock\", std::process::id());\n    let _peer = net_broker(&peer_name, Identity::generate(), &home.path().join(\"b\"));\n\n    // ── Establish the two endpoints ONCE via a test-driver brain, then drop it\n    //    HARD (the D4 hard-kill posture — the outgoing side is gone before the\n    //    supervised child resumes). ──\n    let (sid, child_pid) = {\n        let mut driver = connect_retry(&broker_socket_name());\n\n        // (1) A hosted PTY child the broker holds.\n        let sid = driver.spawn_session(echo_spawn_req()).expect(\"spawn session\");\n        let child_pid = broker.session_pid(sid).expect(\"hosted child has a pid\");\n\n        // (2) A live QUIC connection the broker holds: dial the loopback peer.\n        let peer_addr = {\n            let mut peer_brain = connect_retry(&peer_name);\n            peer_brain.net_status().expect(\"peer status\").addr\n        };\n        driver.net_dial(peer_addr, None).expect(\"dial the loopback peer\");\n\n        // Both endpoints are now broker-held.\n        assert_eq!(broker.session_count(), 1, \"one hosted session established\");\n        assert_eq!(broker.net_conn_count(), 1, \"one live QUIC conn established\");\n        (sid, child_pid)\n        // driver dropped here — HARD detach (no graceful handoff frame).\n    };\n\n    // ── Start the production supervisor, spawning the REAL `spt daemon brain`\n    //    child from the currently-selected fixture (A). NoTrialEnv = the\n    //    pre-D6 respawn loop (no readiness gate); the swap is driven by the\n    //    planned-restart signal, exactly as a real `apply` drives it (D3-3). ──\n    let ready_path = home.path().join(\"brain.ready\");\n    let stop = Arc::new(AtomicBool::new(false));\n    let restart = Arc::new(BrainRestart::new());\n    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n    let home_path = home.path().to_path_buf();\n\n    let sup = {\n        let stop = Arc::clone(&stop);\n        let restart = Arc::clone(&restart);\n        let selected = Arc::clone(&selected);\n        thread::spawn(move || {\n            supervise_brain(\n                &stop,\n                &restart,\n                Duration::from_millis(50),\n                &NoTrialEnv,\n                Duration::from_secs(30),\n                move |gen, reason: StartReason, _binary| {\n                    let exe = selected.lock().unwrap().clone();\n                    Command::new(&exe)\n                        .no_window()\n                        .args([\n                            \"daemon\",\n                            \"brain\",\n                            \"--generation\",\n                            &gen.to_string(),\n                            \"--start-reason\",\n                            reason.as_arg(),\n                        ])\n                        .env(\"SPT_HOME\", &home_path)\n                        .spawn()\n                },\n            );\n        })\n    };\n\n    // ── Generation 0: the real child came up from fixture A, resumed the hosted\n    //    session (the first N≥1 cursor-of-record resume), and stamped exe_hash A. ──\n    let (pid0, _gen0, hash0) = wait_ready(&ready_path, None, Duration::from_secs(45))\n        .unwrap_or_else(|| teardown_panic(&stop, \"gen-0 brain never signalled ready\"));\n    assert_eq!(\n        hash0.as_deref(),\n        Some(hash_a.as_str()),\n        \"gen-0 brain.ready exe_hash must be fixture A\"\n    );\n    assert_eq!(broker.session_count(), 1, \"session survived the driver drop + resume\");\n    assert_eq!(\n        broker.session_pid(sid),\n        Some(child_pid),\n        \"the PTY child's pid is unchanged through the resume\"\n    );\n    assert_eq!(broker.net_conn_count(), 1, \"the QUIC conn survived the driver drop\");\n\n    // ── SWAP: flip the selected binary to B, then trigger a planned restart\n    //    (what `apply` does: swap on disk, signal the brain to cycle). ──\n    *selected.lock().unwrap() = fixture_b.clone();\n    restart.request();\n\n    // ── Generation 1: the supervisor respawned the brain PROCESS onto the\n    //    SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). ──\n    let (pid1, _gen1, hash1) = wait_ready(&ready_path, Some(pid0), Duration::from_secs(45))\n        .unwrap_or_else(|| teardown_panic(&stop, \"gen-1 brain never respawned onto the swap\"));\n    assert_ne!(pid0, pid1, \"the respawned brain is a fresh process\");\n    assert_eq!(\n        hash1.as_deref(),\n        Some(hash_b.as_str()),\n        \"post-swap exe_hash must be fixture B — the supervisor ran the NEW bytes, \\\n         not a relaunch of the old (the property brain_swap.rs cannot prove)\"\n    );\n    assert_ne!(\n        hash1.as_deref(),\n        Some(hash_a.as_str()),\n        \"post-swap exe_hash must differ from the pre-swap binary\"\n    );\n\n    // ── THE SURVIVAL INVARIANT: both broker-held endpoints are untouched across\n    //    the brain-PROCESS swap. ──\n    assert_eq!(\n        broker.session_pid(sid),\n        Some(child_pid),\n        \"REQ-UPD-3: the hosted PTY child's pid is unchanged across the brain-process swap\"\n    );\n    assert_eq!(broker.session_count(), 1, \"exactly one hosted session throughout\");\n    assert_eq!(\n        broker.net_conn_count(),\n        1,\n        \"the live QUIC conn the broker holds survived the brain-process swap (SPIKE-03 in production topology)\"\n    );\n\n    // ── FUNCTIONAL survival (doyle F1, plan choreography 5(d)): \"held\" is not\n    //    \"alive\". Drive a marker through the held PTY child POST-swap and watch the\n    //    broker-side output seq advance — proving the full round-trip (input →\n    //    child → echo → broker ring) works through the swapped-in brain, not that\n    //    a zombie handle merely lingers. The input rides the KIND_INPUT effect verb\n    //    on a FRESH raw connection, which does NOT steal the supervised brain's\n    //    output subscription (it is not an attach). ──\n    let seq_before = broker\n        .session_output_seq(sid)\n        .unwrap_or_else(|| teardown_panic(&stop, \"hosted session vanished before the functional probe\"));\n    {\n        let mut raw = LocalSocketTransport::connect(&broker_socket_name())\n            .unwrap_or_else(|_| teardown_panic(&stop, \"could not open a raw input connection\"));\n        send_hello(&mut raw, Role::Brain)\n            .unwrap_or_else(|_| teardown_panic(&stop, \"raw hello failed\"));\n        let input = InputReq {\n            session_id: sid,\n            data_b64: encode_bytes(b\"D7-1 POST-SWAP FUNCTIONAL PROBE\\r\"),\n            op_id: None,\n            minter: None,\n            ack: true,\n        };\n        write_frame(\n            &mut raw,\n            &Envelope::new(KIND_INPUT, serde_json::to_value(input).expect(\"InputReq serializes\")),\n        )\n        .unwrap_or_else(|_| teardown_panic(&stop, \"could not write the marker input frame\"));\n    }\n    // The echo child (cat/findstr) echoes the marker → output appended → next_seq\n    // advances. Poll the broker's own log high-water.\n    let mut advanced = false;\n    let deadline = Instant::now() + Duration::from_secs(15);\n    while Instant::now() < deadline {\n        if broker.session_output_seq(sid).map(|s| s > seq_before).unwrap_or(false) {\n            advanced = true;\n            break;\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    if !advanced {\n        teardown_panic(\n            &stop,\n            \"the held PTY child produced no output for a post-swap marker — endpoint held but DEAD\",\n        );\n    }\n\n    // ── Teardown: stop the supervisor (kills the live brain child), reap. ──\n    stop.store(true, Ordering::SeqCst);\n    let _ = sup.join();\n    // The hosted child dies with the broker drop at end-of-scope.\n    let _ = broker;\n}\n\n/// Raise `stop` (so the supervisor thread kills any live brain child and exits)\n/// and panic with `msg` — keeps a failing assertion from orphaning a brain.\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n    stop.store(true, Ordering::SeqCst);\n    panic!(\"{msg}\");\n}\n"}],"details":{"displayContent":{"text":"//! Restoration D7-1 (ADR-0018 V5) — **process-level survival across a binary\r\n//! swap**: a hosted PTY child + a live QUIC connection survive a brain-**PROCESS**\r\n//! restart that lands on a **swapped on-disk binary**, with the broker holding\r\n//! both endpoints untouched throughout. This is the `int` E2E that re-points the\r\n//! regression-masked REQ-DAEMON-2 / REQ-UPD-3 evidence off the in-process\r\n//! `brain_swap.rs` shape onto a real process boundary.\r\n//!\r\n//! ## The proof is a TRIANGLE (doyle D7 vet, call 1) — this is leg (b)\r\n//!\r\n//! No single test proves the whole claim; three legs do, and this file is honest\r\n//! only when read alongside the other two:\r\n//!\r\n//! - **Leg (a) — the real-broker-process leg: `crates/spt/tests/brain_split.rs`.**\r\n//!   The REAL `spt daemon run` broker *process* survives a brain kill and respawns\r\n//!   it. That is where the broker's own process-packaging is proven.\r\n//! - **Leg (b) — the survival leg: THIS test.** A real brain *child process* +\r\n//!   a real on-disk binary swap + real broker-**held** endpoints (a PTY child and\r\n//!   a live QUIC conn). The broker is modeled **in-process** — faithful, not a\r\n//!   weakening: the broker is the never-restarting anchor *by design*, so an\r\n//!   in-process [`Broker`] runs the **same Broker code holding the same\r\n//!   resources** the real broker would across a brain swap. The claim under test\r\n//!   is the brain process + the swapped binary + broker-held endpoint survival —\r\n//!   all real here. (Bonus: the in-process broker gives the test direct access to\r\n//!   its session pid / count and QUIC conn count for the assertions.)\r\n//! - **Leg (c) — the field leg: D7-4.** The real daemon, a real `apply`, real\r\n//!   hardware — the `exe_hash` flips and matches the released artifact.\r\n//!\r\n//! DEFERRED upgrade: once the live-agent adapter makes daemon-hosted sessions a\r\n//! production reality, a full-stack single-process E2E (the real `spt daemon run`\r\n//! hosting *and* surviving) becomes reachable — tracked in `docs/DEFERRED.md`,\r\n//! NOT D7 work.\r\n//!\r\n//! ## What makes this STRONGER than `brain_swap.rs`\r\n//!\r\n//! `brain_swap.rs` (REQ-UPD-3's prior `int`) relaunches the brain via an\r\n//! **in-process closure** — same process image, no disk involved. This test runs\r\n//! the **real `spt daemon brain` child process** the production\r\n//! [`supervise_brain`] spawns, swaps the on-disk binary it respawns from, and\r\n//! proves the post-respawn brain is running the **new bytes** via the D7-1\r\n//! `exe_hash` breadcrumb (`exe_hash == fixture-B != fixture-A`). It is also the\r\n//! **first process-level exercise of the D4 cursor-of-record resume with N≥1\r\n//! hosted sessions** (the supervised child re-attaches the test-established PTY\r\n//! session on cold start).\r\n//!\r\n//! The swap is modeled as a **path flip** between two distinct on-disk fixtures\r\n//! (B = A + appended padding — PE and ELF both tolerate trailing data, so behavior\r\n//! is identical but the hash flips). This is faithful to the production swap (the\r\n//! supervisor respawns from whatever the binary *selection* resolves to) and\r\n//! sidesteps the Windows running-exe lock (you cannot overwrite a live `.exe`).\r\n//!\r\n//! **Held AND functional (doyle F1).** Proving the endpoints are *held* (pid /\r\n//! session_count / conn_count unchanged) is not proving they are *alive* — a\r\n//! zombie conn or wedged PTY would pass. So after the swap the test also drives a\r\n//! marker through the held PTY child and watches the broker's output seq advance:\r\n//! the full round-trip (input → child → echo → broker ring) works through the\r\n//! swapped-in brain.\r\n// [int->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [int->REQ-UPD-3]\r\n// [int->REQ-DAEMON-2]\r\n\r\nuse std::path::PathBuf;\r\nuse std::process::Command;\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::{Arc, Mutex};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::brain::Brain;\r\nuse spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};\r\nuse spt_daemon::codec::write_frame;\r\nuse spt_daemon::endpoint::broker_socket_name;\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{encode_bytes, InputReq, SpawnReq, KIND_INPUT};\r\nuse spt_daemon::nethost::{NetConfig, NetHost};\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\r\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\r\nuse spt_proto::identity::Identity;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\n/// A long-lived echo child to host under a broker PTY (the endpoint REQ-UPD-3\r\n/// forbids us from disturbing across the swap). `cat`/`findstr` idle on stdin.\r\nfn echo_spawn_req() -> 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: String::new(),\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/// A hermetic net config (mirrors `netbroker.rs`): relays disabled, no discovery,\r\n/// loopback-only sockets — no real interface touched, no firewall prompt.\r\nfn hermetic(identity: Identity) -> NetConfig {\r\n    NetConfig {\r\n        identity,\r\n        relay: RelayPolicy::Disabled,\r\n        discovery: LocalDiscovery::Off,\r\n        scope: BindScope::Loopback,\r\n        membership: None,\r\n    }\r\n}\r\n\r\n/// Bind a broker (serving on a background thread) that owns a hermetic QUIC\r\n/// endpoint for `identity`, with its effect journal under `dir`.\r\nfn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\r\n    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\r\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\r\n        .expect(\"bind broker with net\");\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_retry(name: &str) -> Brain {\r\n    for _ in 0..400 {\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/// The three breadcrumb fields out of a `brain.ready` body (parsed inline so this\r\n/// test depends on no private brainproc helper). `None` until the file exists and\r\n/// carries a parseable pid.\r\nfn read_ready(path: &std::path::Path) -> Option<(u32, u64, Option<String>)> {\r\n    let body = std::fs::read_to_string(path).ok()?;\r\n    let v: serde_json::Value = serde_json::from_str(&body).ok()?;\r\n    let pid = v.get(\"pid\")?.as_u64()? as u32;\r\n    let generation = v.get(\"generation\").and_then(|g| g.as_u64()).unwrap_or(0);\r\n    let exe_hash = v\r\n        .get(\"exe_hash\")\r\n        .and_then(|h| h.as_str())\r\n        .map(str::to_string);\r\n    Some((pid, generation, exe_hash))\r\n}\r\n\r\n/// Poll `brain.ready` until it holds a pid (optionally one DIFFERENT from `was`),\r\n/// returning `(pid, generation, exe_hash)`. `None` if it never appears in time.\r\nfn wait_ready(\r\n    path: &std::path::Path,\r\n    was: Option<u32>,\r\n    budget: Duration,\r\n) -> Option<(u32, u64, Option<String>)> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some((pid, gen, hash)) = read_ready(path) {\r\n            if was != Some(pid) {\r\n                return Some((pid, gen, hash));\r\n            }\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\n/// Lowercase-hex SHA-256 of a file (the same digest `brain.ready.exe_hash`\r\n/// carries; recomputed independently here to assert the breadcrumb matches the\r\n/// fixture the supervisor actually spawned).\r\nfn file_hash(path: &std::path::Path) -> String {\r\n    use sha2::{Digest, Sha256};\r\n    let bytes = std::fs::read(path).expect(\"read fixture for hashing\");\r\n    let mut h = Sha256::new();\r\n    h.update(&bytes);\r\n    let digest = h.finalize();\r\n    let mut s = String::with_capacity(digest.len() * 2);\r\n    for b in digest {\r\n        s.push_str(&format!(\"{b:02x}\"));\r\n    }\r\n    s\r\n}\r\n\r\n#[test]\r\nfn pty_and_quic_survive_brain_process_restart_onto_swapped_binary() {\r\n    // The in-process broker + brain children both resolve `broker_socket_name()`\r\n    // from SPT_HOME, so the supervised child connects to OUR broker. (The\r\n    // established daemon-test pattern — daemon_e2e.rs et al. set it the same way.)\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    // Single-test binary by design — this is the SOLE `#[test]` here, so it owns\r\n    // the process-global SPT_HOME on its own thread (cross-binary tests are\r\n    // separate processes). A SECOND `#[test]` added to this file MUST bring the\r\n    // env-lock discipline (a shared mutex / serialized home), or it will race this\r\n    // one — the D6 env-race lesson (see the spt-daemon test_home pattern).\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n\r\n    // ── Two fixtures: A, and B = A + appended padding (the on-disk \"swap\"). ──\r\n    // Both are the real `spt` binary, so `daemon brain` runs; B differs only by\r\n    // trailing bytes (behavior identical, exe_hash flips).\r\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n    let suffix = spt_bin.extension().map(|e| format!(\".{}\", e.to_string_lossy())).unwrap_or_default();\r\n    let fixture_a = home.path().join(format!(\"brain-A{suffix}\"));\r\n    let fixture_b = home.path().join(format!(\"brain-B{suffix}\"));\r\n    std::fs::copy(&spt_bin, &fixture_a).expect(\"stage fixture A\");\r\n    std::fs::copy(&spt_bin, &fixture_b).expect(\"stage fixture B\");\r\n    {\r\n        use std::io::Write;\r\n        let mut f = std::fs::OpenOptions::new()\r\n            .append(true)\r\n            .open(&fixture_b)\r\n            .expect(\"open fixture B to append padding\");\r\n        f.write_all(&[0u8; 4096]).expect(\"append padding to fixture B\");\r\n    }\r\n    let hash_a = file_hash(&fixture_a);\r\n    let hash_b = file_hash(&fixture_b);\r\n    assert_ne!(hash_a, hash_b, \"the swap must flip the binary hash\");\r\n\r\n    // ── The broker (leg-b stable kernel) + a loopback QUIC peer. ──\r\n    let broker = net_broker(&broker_socket_name(), Identity::generate(), &home.path().join(\"a\"));\r\n    let peer_name = format!(\"spt-d7-peer-{}.sock\", std::process::id());\r\n    let _peer = net_broker(&peer_name, Identity::generate(), &home.path().join(\"b\"));\r\n\r\n    // ── Establish the two endpoints ONCE via a test-driver brain, then drop it\r\n    //    HARD (the D4 hard-kill posture — the outgoing side is gone before the\r\n    //    supervised child resumes). ──\r\n    let (sid, child_pid) = {\r\n        let mut driver = connect_retry(&broker_socket_name());\r\n\r\n        // (1) A hosted PTY child the broker holds.\r\n        let sid = driver.spawn_session(echo_spawn_req()).expect(\"spawn session\");\r\n        let child_pid = broker.session_pid(sid).expect(\"hosted child has a pid\");\r\n\r\n        // (2) A live QUIC connection the broker holds: dial the loopback peer.\r\n        let peer_addr = {\r\n            let mut peer_brain = connect_retry(&peer_name);\r\n            peer_brain.net_status().expect(\"peer status\").addr\r\n        };\r\n        driver.net_dial(peer_addr, None).expect(\"dial the loopback peer\");\r\n\r\n        // Both endpoints are now broker-held.\r\n        assert_eq!(broker.session_count(), 1, \"one hosted session established\");\r\n        assert_eq!(broker.net_conn_count(), 1, \"one live QUIC conn established\");\r\n        (sid, child_pid)\r\n        // driver dropped here — HARD detach (no graceful handoff frame).\r\n    };\r\n\r\n    // ── Start the production supervisor, spawning the REAL `spt daemon brain`\r\n    //    child from the currently-selected fixture (A). NoTrialEnv = the\r\n    //    pre-D6 respawn loop (no readiness gate); the swap is driven by the\r\n    //    planned-restart signal, exactly as a real `apply` drives it (D3-3). ──\r\n    let ready_path = home.path().join(\"brain.ready\");\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let restart = Arc::new(BrainRestart::new());\r\n    let selected = Arc::new(Mutex::new(fixture_a.clone()));\r\n    let home_path = home.path().to_path_buf();\r\n\r\n    let sup = {\r\n        let stop = Arc::clone(&stop);\r\n        let restart = Arc::clone(&restart);\r\n        let selected = Arc::clone(&selected);\r\n        thread::spawn(move || {\r\n            supervise_brain(\r\n                &stop,\r\n                &restart,\r\n                Duration::from_millis(50),\r\n                &NoTrialEnv,\r\n                Duration::from_secs(30),\r\n                move |gen, reason: StartReason, _binary| {\r\n                    let exe = selected.lock().unwrap().clone();\r\n                    Command::new(&exe)\r\n                        .no_window()\r\n                        .args([\r\n                            \"daemon\",\r\n                            \"brain\",\r\n                            \"--generation\",\r\n                            &gen.to_string(),\r\n                            \"--start-reason\",\r\n                            reason.as_arg(),\r\n                        ])\r\n                        .env(\"SPT_HOME\", &home_path)\r\n                        .spawn()\r\n                },\r\n            );\r\n        })\r\n    };\r\n\r\n    // ── Generation 0: the real child came up from fixture A, resumed the hosted\r\n    //    session (the first N≥1 cursor-of-record resume), and stamped exe_hash A. ──\r\n    let (pid0, _gen0, hash0) = wait_ready(&ready_path, None, Duration::from_secs(45))\r\n        .unwrap_or_else(|| teardown_panic(&stop, \"gen-0 brain never signalled ready\"));\r\n    assert_eq!(\r\n        hash0.as_deref(),\r\n        Some(hash_a.as_str()),\r\n        \"gen-0 brain.ready exe_hash must be fixture A\"\r\n    );\r\n    assert_eq!(broker.session_count(), 1, \"session survived the driver drop + resume\");\r\n    assert_eq!(\r\n        broker.session_pid(sid),\r\n        Some(child_pid),\r\n        \"the PTY child's pid is unchanged through the resume\"\r\n    );\r\n    assert_eq!(broker.net_conn_count(), 1, \"the QUIC conn survived the driver drop\");\r\n\r\n    // ── SWAP: flip the selected binary to B, then trigger a planned restart\r\n    //    (what `apply` does: swap on disk, signal the brain to cycle). ──\r\n    *selected.lock().unwrap() = fixture_b.clone();\r\n    restart.request();\r\n\r\n    // ── Generation 1: the supervisor respawned the brain PROCESS onto the\r\n    //    SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). ──\r\n    let (pid1, _gen1, hash1) = wait_ready(&ready_path, Some(pid0), Duration::from_secs(45))\r\n        .unwrap_or_else(|| teardown_panic(&stop, \"gen-1 brain never respawned onto the swap\"));\r\n    assert_ne!(pid0, pid1, \"the respawned brain is a fresh process\");\r\n    assert_eq!(\r\n        hash1.as_deref(),\r\n        Some(hash_b.as_str()),\r\n        \"post-swap exe_hash must be fixture B — the supervisor ran the NEW bytes, \\\r\n         not a relaunch of the old (the property brain_swap.rs cannot prove)\"\r\n    );\r\n    assert_ne!(\r\n        hash1.as_deref(),\r\n        Some(hash_a.as_str()),\r\n        \"post-swap exe_hash must differ from the pre-swap binary\"\r\n    );\r\n\r\n    // ── THE SURVIVAL INVARIANT: both broker-held endpoints are untouched across\r\n    //    the brain-PROCESS swap. ──\r\n    assert_eq!(\r\n        broker.session_pid(sid),\r\n        Some(child_pid),\r\n        \"REQ-UPD-3: the hosted PTY child's pid is unchanged across the brain-process swap\"\r\n    );\r\n    assert_eq!(broker.session_count(), 1, \"exactly one hosted session throughout\");\r\n    assert_eq!(\r\n        broker.net_conn_count(),\r\n        1,\r\n        \"the live QUIC conn the broker holds survived the brain-process swap (SPIKE-03 in production topology)\"\r\n    );\r\n\r\n    // ── FUNCTIONAL survival (doyle F1, plan choreography 5(d)): \"held\" is not\r\n    //    \"alive\". Drive a marker through the held PTY child POST-swap and watch the\r\n    //    broker-side output seq advance — proving the full round-trip (input →\r\n    //    child → echo → broker ring) works through the swapped-in brain, not that\r\n    //    a zombie handle merely lingers. The input rides the KIND_INPUT effect verb\r\n    //    on a FRESH raw connection, which does NOT steal the supervised brain's\r\n    //    output subscription (it is not an attach). ──\r\n    let seq_before = broker\r\n        .session_output_seq(sid)\r\n        .unwrap_or_else(|| teardown_panic(&stop, \"hosted session vanished before the functional probe\"));\r\n    {\r\n        let mut raw = LocalSocketTransport::connect(&broker_socket_name())\r\n            .unwrap_or_else(|_| teardown_panic(&stop, \"could not open a raw input connection\"));\r\n        send_hello(&mut raw, Role::Brain)\r\n            .unwrap_or_else(|_| teardown_panic(&stop, \"raw hello failed\"));\r\n        let input = InputReq {\r\n            session_id: sid,\r\n            data_b64: encode_bytes(b\"D7-1 POST-SWAP FUNCTIONAL PROBE\\r\"),\r\n            op_id: None,\r\n            minter: None,\r\n            ack: true,\r\n        };\r\n        write_frame(\r\n            &mut raw,\r\n            &Envelope::new(KIND_INPUT, serde_json::to_value(input).expect(\"InputReq serializes\")),\r\n        )\r\n        .unwrap_or_else(|_| teardown_panic(&stop, \"could not write the marker input frame\"));\r\n    }\r\n    // The echo child (cat/findstr) echoes the marker → output appended → next_seq\r\n    // advances. Poll the broker's own log high-water.\r\n    let mut advanced = false;\r\n    let deadline = Instant::now() + Duration::from_secs(15);\r\n    while Instant::now() < deadline {\r\n        if broker.session_output_seq(sid).map(|s| s > seq_before).unwrap_or(false) {\r\n            advanced = true;\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n    if !advanced {\r\n        teardown_panic(\r\n            &stop,\r\n            \"the held PTY child produced no output for a post-swap marker — endpoint held but DEAD\",\r\n        );\r\n    }\r\n\r\n    // ── Teardown: stop the supervisor (kills the live brain child), reap. ──\r\n    stop.store(true, Ordering::SeqCst);\r\n    let _ = sup.join();\r\n    // The hosted child dies with the broker drop at end-of-scope.\r\n    let _ = broker;\r\n}\r\n\r\n/// Raise `stop` (so the supervisor thread kills any live brain child and exits)\r\n/// and panic with `msg` — keeps a failing assertion from orphaning a brain.\r\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\r\n    stop.store(true, Ordering::SeqCst);\r\n    panic!(\"{msg}\");\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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\brain_survive.rs"}}},"isError":false,"timestamp":1784195765901}}
{"type":"message","id":"34923976","parentId":"064c5470","timestamp":"2026-07-16T09:56:05.902Z","message":{"role":"toolResult","toolCallId":"call_FLKOO7dRUl2x1Fa8BNf7Ok6F|fc_0b42584a8ab0545d016a58aab5ca508199a831e9f7e4505205","toolName":"read","content":[{"type":"text","text":"//! Restoration D1 process-level smoke (ADR-0018 Q2/Q3) — the broker/brain\n//! process boundary, proven against the REAL `spt daemon run` binary.\n//!\n//! This is the de-risk spike #1 in production topology: the broker is the\n//! always-up per-machine anchor that spawns + supervises a `spt daemon brain`\n//! child, and **the broker survives the brain dying** — the supervisor respawns\n//! the brain, which reconnects to the still-serving broker socket. A respawned\n//! brain (a NEW pid in `brain.ready`) can only appear if the broker stayed alive\n//! to accept its connection, so the respawn IS the broker-survival proof.\n//!\n//! D1 is a true minimal skeleton: the brain child connects, signals ready, and\n//! idles; the logic loops stay broker-side until D2. So this test asserts only\n//! the boundary — come-up, survive-brain-kill, respawn — not any logic.\n//!\n//! The full PTY-child + live-QUIC survival across a swap is the D7 `int` E2E\n//! (REQ-UPD-3 re-point); here the process-level boundary is `unit`-tagged.\n// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// Best-effort cross-platform kill of a pid (mirrors the other E2E helpers).\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/// Read the `pid` out of a `brain.ready` body. As of restoration D6-1b the\n/// breadcrumb is self-describing JSON `{pid, generation}` (was bare-pid text);\n/// this is the only reader, migrated in the same commit.\nfn ready_pid(body: &str) -> Option<u32> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"pid\")?\n        .as_u64()\n        .map(|p| p as u32)\n}\n\n/// Poll `brain.ready` for a pid, up to `budget`. `None` if it never appears.\nfn wait_for_ready_pid(path: &PathBuf, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Ok(s) = std::fs::read_to_string(path) {\n            if let Some(pid) = ready_pid(&s) {\n                return Some(pid);\n            }\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// Poll `brain.ready` until it holds a pid DIFFERENT from `was` (a respawn),\n/// up to `budget`. `None` if it never changes.\nfn wait_for_new_pid(path: &PathBuf, was: u32, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Ok(s) = std::fs::read_to_string(path) {\n            if let Some(pid) = ready_pid(&s) {\n                if pid != was {\n                    return Some(pid);\n                }\n            }\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// Whether a daemon answers the seed-control channel for `home`, asked the way\n/// any user would — `spt daemon status` (read-only; its liveness line is the\n/// seed-socket ping, `cli.rs::cmd_daemon_status`). True iff stdout reports\n/// `daemon: running` (never a substring of the `daemon: not running` line, so\n/// the match is unambiguous).\nfn daemon_running(spt_bin: &PathBuf, home: &std::path::Path) -> bool {\n    let out = Command::new(spt_bin)\n        .no_window()\n        .args([\"daemon\", \"status\"])\n        .env(\"SPT_HOME\", home)\n        .output()\n        .expect(\"run spt daemon status\");\n    String::from_utf8_lossy(&out.stdout).contains(\"daemon: running\")\n}\n\n/// D3-1 (ADR-0018 Q2) — the seed-control lock + liveness is the broker's, so it\n/// is the **always-up anchor**: a brain restart must never perturb it. The\n/// single-daemon-per-`SPT_HOME` ping (`is_running`, what `ensure_running` /\n/// `daemon stop` target) lives in the broker's foreground loop\n/// (`daemon.rs::Daemon::run`), NOT the brain — so killing the brain and watching\n/// the supervisor respawn it must leave `daemon: running` **continuously** true.\n/// If the seed anchor ever rode the brain, this is the test that would catch it\n/// flapping. External `ensure_running`/`is_running`/`daemon stop` behavior is\n/// unchanged — this only proves the owner process is the survivor.\n// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n#[test]\nfn seed_anchor_survives_brain_cycle() {\n    let home = tempfile::tempdir().unwrap();\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let ready = home.path().join(\"brain.ready\");\n\n    let mut broker: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .spawn()\n        .expect(\"spawn spt daemon run\");\n\n    // Come-up: a brain child is ready AND the seed anchor answers.\n    let pid1 = wait_for_ready_pid(&ready, Duration::from_secs(30)).unwrap_or_else(|| {\n        let _ = broker.kill();\n        panic!(\"brain child never signalled ready\");\n    });\n    assert!(\n        daemon_running(&spt_bin, home.path()),\n        \"seed anchor must answer once the daemon is up\"\n    );\n\n    // Kill the brain; while the supervisor respawns it, the seed anchor (in the\n    // never-restarting broker) must stay continuously up. Sample across the whole\n    // respawn gap — a single not-running read is a failure (the anchor flapped).\n    kill_pid(pid1);\n    let deadline = Instant::now() + Duration::from_secs(30);\n    let mut pid2 = None;\n    while Instant::now() < deadline {\n        assert!(\n            daemon_running(&spt_bin, home.path()),\n            \"seed anchor dropped during a brain respawn — it must not ride the brain\"\n        );\n        if let Ok(s) = std::fs::read_to_string(&ready) {\n            if let Some(pid) = ready_pid(&s) {\n                if pid != pid1 {\n                    pid2 = Some(pid);\n                    break;\n                }\n            }\n        }\n        thread::sleep(Duration::from_millis(100));\n    }\n    let pid2 = pid2.unwrap_or_else(|| {\n        kill_pid(pid1);\n        let _ = broker.kill();\n        panic!(\"supervisor did not respawn the brain\");\n    });\n    assert_ne!(pid1, pid2, \"the respawned brain is a fresh process\");\n    assert!(\n        daemon_running(&spt_bin, home.path()),\n        \"seed anchor must still answer after the brain respawned\"\n    );\n    assert!(\n        matches!(broker.try_wait(), Ok(None)),\n        \"broker must survive the brain cycle (it is the anchor)\"\n    );\n\n    kill_pid(pid2);\n    let _ = broker.kill();\n    let _ = broker.wait();\n}\n\n#[test]\nfn broker_survives_brain_kill_and_respawns_it() {\n    let home = tempfile::tempdir().unwrap();\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let ready = home.path().join(\"brain.ready\");\n\n    // Start the REAL broker process (`spt daemon run`) on an isolated home.\n    let mut broker: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .spawn()\n        .expect(\"spawn spt daemon run\");\n\n    // ── 1. come-up: the broker spawned a brain child that signalled ready ──────\n    let pid1 = wait_for_ready_pid(&ready, Duration::from_secs(30)).unwrap_or_else(|| {\n        let _ = broker.kill();\n        panic!(\"brain child never signalled ready (no brain.ready breadcrumb)\");\n    });\n    assert!(\n        matches!(broker.try_wait(), Ok(None)),\n        \"broker must still be running after the brain came up\"\n    );\n\n    // ── 2. survive-brain-kill + respawn: kill the brain, expect a NEW pid ──────\n    kill_pid(pid1);\n    let pid2 = wait_for_new_pid(&ready, pid1, Duration::from_secs(30)).unwrap_or_else(|| {\n        kill_pid(pid1);\n        let _ = broker.kill();\n        panic!(\"supervisor did not respawn the brain after it was killed\");\n    });\n    assert_ne!(pid1, pid2, \"the respawned brain is a fresh process\");\n\n    // The respawn proves broker survival (a new brain could only connect to a\n    // still-serving broker socket); assert the broker process itself too.\n    assert!(\n        matches!(broker.try_wait(), Ok(None)),\n        \"broker must survive the brain death (it is the always-up anchor)\"\n    );\n\n    // ── teardown ───────────────────────────────────────────────────────────────\n    kill_pid(pid2);\n    let _ = broker.kill();\n    let _ = broker.wait();\n}\n"}],"details":{"displayContent":{"text":"//! Restoration D1 process-level smoke (ADR-0018 Q2/Q3) — the broker/brain\n//! process boundary, proven against the REAL `spt daemon run` binary.\n//!\n//! This is the de-risk spike #1 in production topology: the broker is the\n//! always-up per-machine anchor that spawns + supervises a `spt daemon brain`\n//! child, and **the broker survives the brain dying** — the supervisor respawns\n//! the brain, which reconnects to the still-serving broker socket. A respawned\n//! brain (a NEW pid in `brain.ready`) can only appear if the broker stayed alive\n//! to accept its connection, so the respawn IS the broker-survival proof.\n//!\n//! D1 is a true minimal skeleton: the brain child connects, signals ready, and\n//! idles; the logic loops stay broker-side until D2. So this test asserts only\n//! the boundary — come-up, survive-brain-kill, respawn — not any logic.\n//!\n//! The full PTY-child + live-QUIC survival across a swap is the D7 `int` E2E\n//! (REQ-UPD-3 re-point); here the process-level boundary is `unit`-tagged.\n// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// Best-effort cross-platform kill of a pid (mirrors the other E2E helpers).\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/// Read the `pid` out of a `brain.ready` body. As of restoration D6-1b the\n/// breadcrumb is self-describing JSON `{pid, generation}` (was bare-pid text);\n/// this is the only reader, migrated in the same commit.\nfn ready_pid(body: &str) -> Option<u32> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"pid\")?\n        .as_u64()\n        .map(|p| p as u32)\n}\n\n/// Poll `brain.ready` for a pid, up to `budget`. `None` if it never appears.\nfn wait_for_ready_pid(path: &PathBuf, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Ok(s) = std::fs::read_to_string(path) {\n            if let Some(pid) = ready_pid(&s) {\n                return Some(pid);\n            }\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// Poll `brain.ready` until it holds a pid DIFFERENT from `was` (a respawn),\n/// up to `budget`. `None` if it never changes.\nfn wait_for_new_pid(path: &PathBuf, was: u32, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Ok(s) = std::fs::read_to_string(path) {\n            if let Some(pid) = ready_pid(&s) {\n                if pid != was {\n                    return Some(pid);\n                }\n            }\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// Whether a daemon answers the seed-control channel for `home`, asked the way\n/// any user would — `spt daemon status` (read-only; its liveness line is the\n/// seed-socket ping, `cli.rs::cmd_daemon_status`). True iff stdout reports\n/// `daemon: running` (never a substring of the `daemon: not running` line, so\n/// the match is unambiguous).\nfn daemon_running(spt_bin: &PathBuf, home: &std::path::Path) -> bool {\n    let out = Command::new(spt_bin)\n        .no_window()\n        .args([\"daemon\", \"status\"])\n        .env(\"SPT_HOME\", home)\n        .output()\n        .expect(\"run spt daemon status\");\n    String::from_utf8_lossy(&out.stdout).contains(\"daemon: running\")\n}\n\n/// D3-1 (ADR-0018 Q2) — the seed-control lock + liveness is the broker's, so it\n/// is the **always-up anchor**: a brain restart must never perturb it. The\n/// single-daemon-per-`SPT_HOME` ping (`is_running`, what `ensure_running` /\n/// `daemon stop` target) lives in the broker's foreground loop\n/// (`daemon.rs::Daemon::run`), NOT the brain — so killing the brain and watching\n/// the supervisor respawn it must leave `daemon: running` **continuously** true.\n/// If the seed anchor ever rode the brain, this is the test that would catch it\n/// flapping. External `ensure_running`/`is_running`/`daemon stop` behavior is\n/// unchanged — this only proves the owner process is the survivor.\n// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n#[test]\nfn seed_anchor_survives_brain_cycle() {\n    let home = tempfile::tempdir().unwrap();\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let ready = home.path().join(\"brain.ready\");\n\n    let mut broker: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .spawn()\n        .expect(\"spawn spt daemon run\");\n\n    // Come-up: a brain child is ready AND the seed anchor answers.\n    let pid1 = wait_for_ready_pid(&ready, Duration::from_secs(30)).unwrap_or_else(|| {\n        let _ = broker.kill();\n        panic!(\"brain child never signalled ready\");\n    });\n    assert!(\n        daemon_running(&spt_bin, home.path()),\n        \"seed anchor must answer once the daemon is up\"\n    );\n\n    // Kill the brain; while the supervisor respawns it, the seed anchor (in the\n    // never-restarting broker) must stay continuously up. Sample across the whole\n    // respawn gap — a single not-running read is a failure (the anchor flapped).\n    kill_pid(pid1);\n    let deadline = Instant::now() + Duration::from_secs(30);\n    let mut pid2 = None;\n    while Instant::now() < deadline {\n        assert!(\n            daemon_running(&spt_bin, home.path()),\n            \"seed anchor dropped during a brain respawn — it must not ride the brain\"\n        );\n        if let Ok(s) = std::fs::read_to_string(&ready) {\n            if let Some(pid) = ready_pid(&s) {\n                if pid != pid1 {\n                    pid2 = Some(pid);\n                    break;\n                }\n            }\n        }\n        thread::sleep(Duration::from_millis(100));\n    }\n    let pid2 = pid2.unwrap_or_else(|| {\n        kill_pid(pid1);\n        let _ = broker.kill();\n        panic!(\"supervisor did not respawn the brain\");\n    });\n    assert_ne!(pid1, pid2, \"the respawned brain is a fresh process\");\n    assert!(\n        daemon_running(&spt_bin, home.path()),\n        \"seed anchor must still answer after the brain respawned\"\n    );\n    assert!(\n        matches!(broker.try_wait(), Ok(None)),\n        \"broker must survive the brain cycle (it is the anchor)\"\n    );\n\n    kill_pid(pid2);\n    let _ = broker.kill();\n    let _ = broker.wait();\n}\n\n#[test]\nfn broker_survives_brain_kill_and_respawns_it() {\n    let home = tempfile::tempdir().unwrap();\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let ready = home.path().join(\"brain.ready\");\n\n    // Start the REAL broker process (`spt daemon run`) on an isolated home.\n    let mut broker: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .spawn()\n        .expect(\"spawn spt daemon run\");\n\n    // ── 1. come-up: the broker spawned a brain child that signalled ready ──────\n    let pid1 = wait_for_ready_pid(&ready, Duration::from_secs(30)).unwrap_or_else(|| {\n        let _ = broker.kill();\n        panic!(\"brain child never signalled ready (no brain.ready breadcrumb)\");\n    });\n    assert!(\n        matches!(broker.try_wait(), Ok(None)),\n        \"broker must still be running after the brain came up\"\n    );\n\n    // ── 2. survive-brain-kill + respawn: kill the brain, expect a NEW pid ──────\n    kill_pid(pid1);\n    let pid2 = wait_for_new_pid(&ready, pid1, Duration::from_secs(30)).unwrap_or_else(|| {\n        kill_pid(pid1);\n        let _ = broker.kill();\n        panic!(\"supervisor did not respawn the brain after it was killed\");\n    });\n    assert_ne!(pid1, pid2, \"the respawned brain is a fresh process\");\n\n    // The respawn proves broker survival (a new brain could only connect to a\n    // still-serving broker socket); assert the broker process itself too.\n    assert!(\n        matches!(broker.try_wait(), Ok(None)),\n        \"broker must survive the brain death (it is the always-up anchor)\"\n    );\n\n    // ── teardown ───────────────────────────────────────────────────────────────\n    kill_pid(pid2);\n    let _ = broker.kill();\n    let _ = broker.wait();\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\brain_split.rs"}}},"isError":false,"timestamp":1784195765902}}
{"type":"message","id":"92b5a7e9","parentId":"34923976","timestamp":"2026-07-16T09:56:05.988Z","message":{"role":"toolResult","toolCallId":"call_qsT7irDnwTv10LOqNTaE2H2T|fc_0b42584a8ab0545d016a58aab5ca6c819986ab852ec3b3188d","toolName":"grep","content":[{"type":"text","text":"# docs/\n## KNOWN-HAZARDS.md\n 4|\n*5|**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...\n 6|\n 7|---\n 8|\n...\n 14|- **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*15|- **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 16|\n 17|### 1.2 Poll-rewrite race & info.json mid-write reads\n 18|- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n...\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 26|- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n*27|- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n 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...\n 32|- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n*33|- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\n 34|\n*35|### 1.5 Worker (working-perch) lifecycle path consistency\n*36|- **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*37|- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\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 46|- **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*47|- **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 48|- **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*49|- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n 50|\n 51|### 2.2 Stdin session_id precedence over env\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...\n 60|- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n*61|- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n 62|\n 63|### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n 64|- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n...\n 67|- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n*68|- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n 69|\n 70|### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n*71|- **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 `...\n*72|- **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** (`inf...\n*73|- **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 ...\n*74|- **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 75|\n 76|---\n 77|\n...\n 79|\n*80|### 3.1 Ephemeral perch cleanup on every `ring` exit path\n*81|- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\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 32|| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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...\n 42|| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psy...\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...\n 54||---|---|---|---|---|\n*55|| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n 56|| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57|| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58|| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n 59|| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60|| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61|| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n\n## adr/\n### 0018-broker-brain-process-isolation-restoration.md\n 10|\n*11|On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12|\n 13|Consequences of the drift:\n 14|- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n*15|- `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 16|- 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 17|- **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 18|\n...\n 20|\n*21|**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 22|\n 23|1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n 24|2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25|3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n*26|4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27|5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n 28|6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29|7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n\n# crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 10|//! and does NOT `wan_send`. This gate locks that: with the target resolvable ONLY on\n*11|//! a REMOTE node (a WAN snapshot, no local perch) so a `default` send WOULD take the\n 12|//! WAN leg, an `--active-only` send must refuse local-only and never touch WAN.\n 13|//!\n 14|//! Daemon-free: the active_only path returns before any broker interaction; the\n...\n 27|use spt_net::net::registry::{Instance, Status, SubnetRegistry};\n*28|use spt_store::perch;\n 29|\n 30|// The body does in-proc SubnetStore/nodeid/snapshot writes keyed off the\n 31|// process-global SPT_HOME — serialize so a future sibling can't race the env.\n...\n 60|/// Seed a subnet + a WAN registry snapshot placing `id` on a REMOTE node — so a\n*61|/// `default` send resolves it to the WAN leg (there is no local perch).\n 62|fn seed_remote_only(subnet: &str, id: &str) {\n 63|    let mut store = spt_store::subnet::SubnetStore::load();\n 64|    store.create_subnet(subnet).expect(\"seed subnet\");\n...\n 66|\n*67|    let dir = perch::identity_dir().join(\"registry\");\n 68|    std::fs::create_dir_all(&dir).unwrap();\n 69|    let mut reg = SubnetRegistry::new();\n 70|    reg.merge_instance(\n...\n 76|            epoch: 1,\n*77|            resources: None,\n 78|            last_active_ms: None,\n 79|            shell_adapters: Vec::new(),\n 80|            node_label: None,\n## adapter_post_step.rs\n 5|//!\n*6|//! The GitHub latest-version query is short-circuited by the `SPT_TEST_GH_LATEST`\n*7|//! seam set to the installed version, so the adapter is a version NO-OP — proving\n*8|//! the post-step runs UNCONDITIONALLY (even with no pull). Each mode exercises one\n 9|//! arm of the stdout notice arbitration: custom text SUPERSEDES `[update].message`;\n 10|//! the reserved sentinel FIRES the static `[update].message`; empty = no notice; a\n 11|//! nonzero exit is loud (overall exit nonzero) and failure-isolated (no rollback —\n...\n 20|\n*21|use spt_store::perch;\n 22|\n 23|fn sibling_bin(name: &str) -> PathBuf {\n 24|    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n...\n 32|        .env(\"SPT_HOME\", home)\n*33|        // No-op: latest == installed → the pull is skipped, the post-step still runs.\n 34|        .env(\"SPT_TEST_GH_LATEST\", \"1.0.0\")\n 35|        .env(\"POST_FIXTURE_MODE\", mode)\n 36|        .env(\"POST_FIXTURE_SEAM_OUT\", seam_out)\n...\n 53|    std::env::set_var(\"SPT_HOME\", home.path());\n*54|    let adapters = perch::adapters_dir();\n 55|\n 56|    // A gh_release adapter declaring [update.post] (the fixture) AND a static\n 57|    // [update].message (so the sentinel arm has something to fire).\n...\n 64|    );\n*65|    let src = perch::spt_home().join(\"srcs\").join(\"cc\");\n*66|    std::fs::create_dir_all(&src).unwrap();\n*67|    std::fs::write(src.join(\"manifest.toml\"), &manifest).unwrap();\n*68|    spt_runtime::registry::register(&adapters, &src, 1).unwrap();\n 69|    std::env::remove_var(\"SPT_HOME\");\n 70|\n 71|    let seam_out = home.path().join(\"seam.json\");\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 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 21|//! residual loopback write. This test is the faithful real-daemon proof that the wedge\n 22|//! does not occur — and folds in L3 (dead endpoint offlined within one\n...\n 35|use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};\n*36|use spt_store::perch::{self, ParentHint};\n 37|\n 38|fn kill_pid(pid: u32) {\n 39|    #[cfg(windows)]\n...\n 100|\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*106|        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);\n 107|        if now.as_deref() == Some(want) {\n 108|            return true;\n 109|        }\n...\n 114|\n*115|/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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 125|        .env(\"SPT_HOME\", home)\n 126|        .stdout(Stdio::piped())\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*132|    let mut rc_out = rc.stdout.take().unwrap();\n 133|    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 134|    let reader = std::thread::spawn(move || {\n 135|        let mut buf = [0u8; 4096];\n...\n 137|        loop {\n*138|            match rc_out.read(&mut buf) {\n 139|                Ok(0) => break,\n 140|                Ok(n) => {\n 141|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\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## bind_adapter_profile_persist_e2e.rs\n 14|//! This drives the REAL bind path — `spt api bind`, which routes through\n*15|//! `establish_perch` → `stamp_creation_fields` — over the REAL `spt` binary (`spt` is\n*16|//! bin-only, so the binary IS the only entry to the real bind path). A perch\n 17|//! pre-stamped `<parent>:<profile>` is re-bound with an incoming BARE `<parent>`\n 18|//! (`api --adapter <parent>`, threaded verbatim to `cmd_bind`) — exactly the value\n 19|//! the agnostic hook-bind resolves; passing it explicitly reproduces the same incoming\n...\n 34|use spt_store::info::{self, InfoJson};\n*35|use spt_store::perch::{self, ParentHint};\n 36|\n 37|fn start_inproc_daemon() {\n*38|    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n 39|    let name = spt_daemon::seed_socket_name();\n 40|    std::thread::spawn(move || {\n 41|        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n...\n 61|\n*62|/// Create a top-level LIVE_AGENT perch pinned to `session_id` with a pre-stamped\n 63|/// composite `adapter` — the \"created with `<parent>:<profile>`\" starting state.\n 64|/// Owner pid = THIS process (alive): a re-bind under the SAME session id is a\n 65|/// reconnect (not a cross-session conflict), so the bind proceeds and re-stamps.\n*66|fn seed_perch_with_adapter(id: &str, session_id: &str, adapter: &str) {\n*67|    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n 68|    std::fs::create_dir_all(&path).unwrap();\n 69|    let mut rec = InfoJson::new(\n 70|        id,\n...\n 80|fn recorded_adapter(id: &str) -> Option<String> {\n*81|    info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))\n 82|        .expect(\"info present\")\n 83|        .adapter\n 84|}\n...\n 101|    std::fs::write(&manifest_path, manifest_toml).unwrap();\n*102|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*103|    std::fs::create_dir_all(&src).unwrap();\n*104|    std::fs::write(src.join(\"manifest.toml\"), manifest_toml).unwrap();\n*105|    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 106|    let mp = manifest_path.to_string_lossy().to_string();\n 107|\n*108|    // A perch CREATED with a composite adapter `<parent>:<profile>`.\n 109|    let id = \"prof-ep\";\n 110|    let sid = \"prof-ep-sid\";\n 111|    let created_adapter = \"dummyharness:fast\";\n 112|    let parent_only = \"dummyharness\"; // what the agnostic hook-bind resolves.\n*113|    seed_perch_with_adapter(id, sid, created_adapter);\n 114|\n*115|    // Precondition: the perch starts stamped with the FULL composite option.\n 116|    assert_eq!(\n 117|        recorded_adapter(id).as_deref(),\n 118|        Some(created_adapter),\n*119|        \"PRECONDITION: the perch must start stamped with the composite adapter\"\n 120|    );\n 121|\n 122|    // THE RE-BIND, via the REAL bind path: `api bind` presenting the SAME session id\n 123|    // (a reconnect, not a conflict) with an incoming BARE `<parent>` adapter — exactly\n*124|    // what the agnostic hook-bind resolves. `bind` routes establish_perch →\n 125|    // stamp_creation_fields; the profile-preserving precedence must KEEP the composite.\n 126|    let out = {\n 127|        let mut cmd = Command::new(&spt_bin);\n## bind_cwd_project_e2e.rs\n 8|//! `dummy_harness_e2e.rs` — `spt endpoint run --start` → broker PTY → the dummy\n*9|//! harness shells `spt api bind` — and then asserts the live perch records a\n 10|//! non-empty `info.cwd`, and that the cwd-derived project membership (the exact\n 11|//! `project_id_for_dir` + origin-union rule the picker's `local_rows` runs)\n 12|//! places the endpoint under its project.\n...\n 18|//! The pure union rule is unit-tested in `picker/data.rs`\n*19|//! (`origin_project_unions_into_membership`); this test proves the REAL perch\n 20|//! carries the cwd that rule consumes.\n 21|//!\n 22|//! Bounded; all spawned pids reaped SCOPED (never machine-wide — shared runner).\n...\n 30|\n*31|use spt_store::perch::{self, ParentHint};\n 32|\n 33|fn kill_pid(pid: u32) {\n 34|    #[cfg(windows)]\n...\n 93|// [int->REQ-HAZARD-BIND-CWD-UNSET] a REAL spt-hosted endpoint-run dummy-harness\n*94|// perch records a non-empty `info.cwd` (was NEVER set pre-W3 — the refuted P1),\n 95|// and the cwd-derived project membership (the picker's data path) places the\n 96|// endpoint under its project. The fresh endpoint, with no committed context,\n 97|// appears under its origin project SOLELY because of the recorded cwd.\n...\n 119|    //    dummy_harness_e2e.rs: a long-lived live_agent fixture that binds its\n*120|    //    perch + heartbeats, declaring [session.psyche_init] so cmd_bind marks\n 121|    //    it online). ──\n 122|    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\n 123|    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\n...\n 127|    );\n*128|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*129|    std::fs::create_dir_all(&src).unwrap();\n*130|    let psyche_bin = src.join(format!(\"psychebin{exe_suffix}\"));\n 131|    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n 132|    let drops = home.path().join(\"drops\");\n 133|    std::fs::create_dir_all(&drops).unwrap();\n...\n 140|    );\n*141|    std::fs::write(src.join(\"manifest.toml\"), &manifest_toml).unwrap();\n*142|    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 143|\n 144|    // ── (3) The REAL daemon broker process. The broker inherits THIS process's\n 145|    //    cwd; the W1 endpoint-run shortcut launches the harness with cwd=None\n...\n 181|\n*182|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n*183|    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\n 184|\n*185|    // Wait for the dummy to bind its perch ONLINE (proves the bind ran — and a\n 186|    // bind is where cwd is stamped).\n 187|    let mut online = false;\n 188|    let deadline = Instant::now() + Duration::from_secs(20);\n...\n 196|\n*197|    // Read the bound perch's recorded cwd (the W3 field).\n*198|    let info = spt_store::info::read_info(&self_perch);\n 199|    let recorded_cwd = info.as_ref().and_then(|i| i.cwd.clone());\n 200|\n 201|    // The harness pid (long-lived PTY child) — reap target.\n...\n 223|    }\n*224|    let psyche_perch =\n*225|        perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\n*226|    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\n 227|        kill_pid(p);\n 228|    }\n 229|    let _ = {\n...\n 244|        online,\n*245|        \"the dummy harness must bind its perch ONLINE (the bind is where cwd is stamped)\"\n 246|    );\n 247|\n*248|    // The real-fixture gate: a freshly bound perch records a non-empty cwd.\n 249|    let cwd = recorded_cwd.expect(\n## bind_honest_cross_perch_e2e.rs\n 2|//! contamination is structurally impossible: the dead-owner re-pin rescue REFUSES\n*3|//! to rebind a perch when the incoming session id AFFIRMATIVELY owns ANOTHER perch.\n 4|//!\n*5|//! The field contamination (doyle RCA 2026-07-03): a nested psyche's SessionStart\n*6|//! hook mis-resolved its \"self\" id to a FOREIGN, DEAD top-level perch (hall-a, whose\n 7|//! harness had exited) and fired an id-scoped `api` call presenting the PSYCHE's own\n 8|//! session id. The dead-owner rescue (REQ-HAZARD-SESSION-PIN-WEDGE) then re-pinned\n 9|//! hall-a's `info.json.session_id` to the psyche's session (359d7bd7) — the observed\n*10|//! corruption. The fix (`auth.rs` `sid_owns_another_perch`): a re-pin whose incoming\n*11|//! sid has an HONEST HOME on another perch is a cross-perch mis-stamp, never a\n 12|//! same-session reconnect — so it is refused and the victim's pin is untouched.\n 13|//!\n 14|//! This drives the REAL auth path — `spt api presence`, which routes through\n*15|//! `gated()` → `authenticate()` — over the REAL `spt` binary against a REAL detached\n 16|//! `spt daemon run`. `spt` is bin-only (no lib), so the binary IS the only entry to\n*17|//! the real auth path; there is no mock. TWO perches: victim A (dead owner) and the\n 18|//! session's honest home B. An `api presence --id A --session-id <B's sid>` must be\n 19|//! REFUSED and leave A's pin untouched (no contamination stamp lands on A).\n 20|//!\n...\n 30|use spt_store::info::{self, InfoJson};\n*31|use spt_store::perch::{self, ParentHint};\n 32|\n 33|/// A DEFINITELY-DEAD pid (above Linux pid_max and Windows ranges; positive-as-i32\n 34|/// so Unix kill-group semantics don't misfire). Mirrors the auth unit's DEAD_PID —\n...\n 77|\n*78|/// Create a top-level perch pinned to `session_id`, recording owner `pid`.\n*79|fn seed_perch(id: &str, session_id: &str, pid: u32, state: &str) -> PathBuf {\n*80|    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n 81|    std::fs::create_dir_all(&path).unwrap();\n 82|    info::write_info(&path, &InfoJson::new(id, \"2026-06-01T00:00:00Z\", pid, session_id, state)).unwrap();\n 83|    path\n...\n 90|#[test]\n*91|fn cross_perch_dead_owner_repin_is_refused_no_contamination() {\n 92|    // ── (1) Isolated, NET-LESS SPT_HOME (auth is pure filesystem; no net needed). ──\n 93|    let home = tempfile::tempdir().unwrap();\n 94|    std::env::set_var(\"SPT_HOME\", home.path());\n...\n 100|\n*101|    // ── (2) TWO perches. ──\n*102|    // Victim A: a foreign top-level LIVE_AGENT perch whose owner pid is DEAD (its\n*103|    // harness exited) — the perch the dead-owner rescue would otherwise re-pin.\n 104|    let victim_id = \"hall-a\";\n 105|    let victim_sid = \"hall-a-own-sid\";\n*106|    let victim = seed_perch(victim_id, victim_sid, DEAD_PID, \"live_agent\");\n*107|    // The session's HONEST HOME B: a perch the incoming sid AFFIRMATIVELY owns (the\n 108|    // nested psyche that legitimately holds 359d7bd7 in the field). Its liveness is\n 109|    // irrelevant to the guard — what matters is that the sid has a home elsewhere.\n 110|    let foreign_sid = \"359d7bd7\";\n*111|    let owner_b = seed_perch(\"sess-owner-b\", foreign_sid, std::process::id(), \"psyche\");\n 112|\n 113|    // ── (3) The REAL daemon broker process (doyle: real daemon; also keeps\n 114|    //    `ensure_daemon` on the api call a no-op so no stray daemon leaks). ──\n...\n 146|        foreign_sid,\n*147|        \"PRECONDITION: the honest-home perch must own the incoming sid\"\n 148|    );\n 149|\n 150|    // ── (4) THE ATTACK, via the REAL auth path: an id-scoped `api presence --id A`\n...\n 152|    //    psyche hook fired. `presence` routes gated() → authenticate(); the dead-owner\n*153|    //    rescue must REFUSE because the sid owns another perch (bind honesty). ──\n 154|    let attack = {\n 155|        let mut cmd = Command::new(&spt_bin);\n 156|        cmd.no_window()\n## bootstrap_verb_e2e.rs\n*1|//! THE-FORKENING W1 — the self-install verb, driven end to end as a virgin\n 2|//! box would use it (ADR-0036 §3, bootstrap path b): the release binary is\n*3|//! \"downloaded\" under its `spt-*` asset name (never an installer-detection\n*4|//! trigger word), runs `install` against a clean simulated `SPT_HOME`, lands\n 5|//! at the canonical `<SPT_HOME>/bin` path, is idempotent on re-run, and the\n*6|//! PLACED binary's `spt update fetch` speaks the gh channel — proven by the\n 7|//! loud `GhCliRequired` refusal when no `gh` is resolvable, which is the gh\n 8|//! carrier gate itself, not an HTTPS fallback.\n 9|//!\n...\n 12|//!\n*13|//! Named `bootstrap_verb_e2e`, NOT `install_verb_e2e`: an exe whose NAME\n*14|//! carries install/setup/update trips Windows installer-detection (UAC os\n 15|//! error 740) — the test binary itself proved the gotcha the verb's `spt-*`\n 16|//! asset-name rule exists to dodge.\n 17|//\n*18|// [int->REQ-INSTALL-BOOTSTRAP-VERB]\n 19|\n 20|use std::process::Command;\n 21|\n...\n 30|\n*31|fn installed_name() -> &'static str {\n 32|    if cfg!(windows) {\n 33|        \"spt.exe\"\n 34|    } else {\n...\n 39|#[test]\n*40|fn downloaded_binary_self_installs_and_speaks_gh_channel() {\n 41|    let tmp = tempfile::tempdir().expect(\"tempdir\");\n 42|    let home = tmp.path().join(\"spt-home\");\n 43|    std::fs::create_dir_all(&home).unwrap();\n...\n 48|\n*49|    // 1. Self-install into the clean home (default dir = <SPT_HOME>/bin).\n 50|    let out = Command::new(&downloaded)\n*51|        .args([\"install\", \"--no-path\"])\n 52|        .env(\"SPT_HOME\", &home)\n 53|        .output()\n*54|        .expect(\"run install\");\n 55|    let stdout = String::from_utf8_lossy(&out.stdout);\n 56|    assert!(\n 57|        out.status.success(),\n*58|        \"install failed: {stdout} {}\",\n 59|        String::from_utf8_lossy(&out.stderr)\n 60|    );\n*61|    let target = home.join(\"bin\").join(installed_name());\n 62|    assert!(target.is_file(), \"binary placed at the canonical path\");\n*63|    assert!(stdout.contains(\"INSTALL_OK\"), \"{stdout}\");\n 64|\n 65|    // 2. Idempotent re-run — still green, same target.\n 66|    let out2 = Command::new(&downloaded)\n*67|        .args([\"install\", \"--no-path\"])\n 68|        .env(\"SPT_HOME\", &home)\n 69|        .output()\n*70|        .expect(\"re-run install\");\n 71|    assert!(\n 72|        out2.status.success(),\n 73|        \"re-run must stay green: {}\",\n...\n 77|    // 3. The PLACED binary works and its update fetch speaks the gh channel:\n*78|    //    with no resolvable gh the carrier gate refuses LOUD with the install\n 79|    //    hint — the gh mandate itself, before any network or key work.\n 80|    let empty_path = tmp.path().join(\"empty-path-dir\");\n 81|    std::fs::create_dir_all(&empty_path).unwrap();\n...\n 86|        .output()\n*87|        .expect(\"run placed spt update fetch\");\n 88|    let stderr = String::from_utf8_lossy(&fetch.stderr);\n 89|    assert!(!fetch.status.success(), \"fetch without gh must refuse\");\n 90|    assert!(\n## boundary_ready_strand_e2e.rs\n 1|//! F-029 C-2 (REQ-HAZARD-BOUNDARY-READY-STRAND) — SEAM-2, B6's second half. A\n*2|//! `/clear` boundary must LEAVE the perch a live inject target: the pre-boundary\n 3|//! soft `api session-end` (which CC fires for the DEPARTING session before\n 4|//! SessionStart, while its sid still matches the pin) removes the ready marker; the\n*5|//! boundary must re-stamp it, or every post-clear force-native (incl. the checkpoint\n 6|//! FIRE) reports the generic UNDELIVERED forever (is_online false → the CLI inject\n 7|//! gate Nones before any broker RPC).\n 8|//!\n 9|//! This drives the REAL boundary path over the REAL `spt` binary: a bound,\n*10|//! controllable, online perch → real soft `api session-end` (ready gone,\n 11|//! is_spt_hosted_no_relay false) → real `api boundary clear` → ready PRESENT +\n 12|//! is_spt_hosted_no_relay TRUE again. RED on 0.23.0 (the boundary leaves ready\n 13|//! absent). Any auto-started daemon against the throwaway home is reaped scoped.\n...\n 23|use spt_store::liveness::STATUS_ONLINE;\n*24|use spt_store::perch::{self, ParentHint};\n 25|\n 26|fn start_inproc_daemon() {\n*27|    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n 28|    let name = spt_daemon::seed_socket_name();\n 29|    std::thread::spawn(move || {\n 30|        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n...\n 50|\n*51|/// Seed a fully BOUND spt-hosted perch: online + controllable + ready marker +\n 52|/// live owner pid (this process) — an `is_spt_hosted_no_relay` inject target.\n*53|fn seed_bound_perch(id: &str, session_id: &str) {\n*54|    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n 55|    std::fs::create_dir_all(&path).unwrap();\n 56|    let mut rec = InfoJson::new(\n 57|        id,\n...\n 65|    info::write_info(&path, &rec).unwrap();\n*66|    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 67|}\n 68|\n 69|#[test]\n...\n 81|    std::fs::write(&manifest_path, manifest_toml).unwrap();\n*82|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*83|    std::fs::create_dir_all(&src).unwrap();\n*84|    std::fs::write(src.join(\"manifest.toml\"), manifest_toml).unwrap();\n*85|    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 86|    let mp = manifest_path.to_string_lossy().to_string();\n 87|\n 88|    let id = \"clr-ep\";\n...\n 90|    let new_sid = \"clr-ep-sid-2\";\n*91|    seed_bound_perch(id, sid);\n 92|\n*93|    let owlery = perch::owlery_dir();\n*94|    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n 95|    let spt = |args: &[&str]| {\n 96|        let mut cmd = Command::new(&spt_bin);\n 97|        cmd.no_window()\n...\n 103|\n*104|    // Precondition: a bound perch is a live inject target.\n 105|    assert!(\n 106|        spt_daemon::is_spt_hosted_no_relay(id, &owlery),\n*107|        \"PRECONDITION: a bound online+controllable+ready perch is an inject target\"\n 108|    );\n 109|\n 110|    // ── The pre-boundary soft session-end (CC fires it for the departing session,\n...\n 117|        !spt_daemon::is_spt_hosted_no_relay(id, &owlery),\n*118|        \"with ready gone the perch is NO LONGER an inject target (the SEAM-2 strand)\"\n 119|    );\n 120|\n 121|    // ── THE BOUNDARY: rotates the sid AND (C-2) re-stamps ready. REAL `api boundary\n...\n 130|    let target_after = spt_daemon::is_spt_hosted_no_relay(id, &owlery);\n*131|    let sid_after = info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))\n 132|        .map(|r| r.session_id);\n 133|    eprintln!(\n 134|        \"=== F029 C-2 DIAGNOSTIC: boundary_exit={:?} ready_after={ready_after} \\\n## brain_respawn_rename.rs\n 7|//! distinct fixtures through an injected spawn closure — it never renames a\n*8|//! *running* binary, so it cannot exercise the production respawn resolution\n 9|//! (`std::env::current_exe()` in the resident broker). That gap let the v0.4.1\n 10|//! Linux brain-respawn bug ship green: on Linux `current_exe()` is\n 11|//! `readlink(/proc/self/exe)`, **inode-tracking**, so it FOLLOWS the `apply`\n...\n 64|\n*65|/// Lowercase-hex SHA-256 of a file — the digest `brain.ready.exe_hash` carries.\n 66|fn file_hash(path: &Path) -> String {\n 67|    use sha2::{Digest, Sha256};\n 68|    let bytes = std::fs::read(path).expect(\"read file for hashing\");\n...\n 94|\n*95|    // The canonical install path P the broker runs from — so the broker's own\n 96|    // current_exe IS P (the resident-broker condition the bug needs).\n 97|    let canonical = bindir.join(format!(\"spt{suffix}\"));\n*98|    std::fs::copy(&real, &canonical).expect(\"install bytes A at canonical P\");\n 99|    let hash_a = file_hash(&canonical);\n 100|\n 101|    // The REAL broker process, launched FROM P.\n...\n 117|        Some(hash_a.as_str()),\n*118|        \"gen-0 brain runs bytes A (the canonical install)\"\n 119|    );\n 120|\n 121|    // ── The real apply choreography on disk: rename the running canonical binary\n 122|    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\n*123|    //    what `spt update apply` does (rename-then-replace). ──\n 124|    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\n 125|    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\n 126|    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\n## brain_survive.rs\n 20|//!   in-process [`Broker`] runs the **same Broker code holding the same\n*21|//!   resources** the real broker would across a brain swap. The claim under test\n 22|//!   is the brain process + the swapped binary + broker-held endpoint survival —\n 23|//!   all real here. (Bonus: the in-process broker gives the test direct access to\n 24|//!   its session pid / count and QUIC conn count for the assertions.)\n...\n 40|//! `exe_hash` breadcrumb (`exe_hash == fixture-B != fixture-A`). It is also the\n*41|//! **first process-level exercise of the D4 cursor-of-record resume with N≥1\n 42|//! hosted sessions** (the supervised child re-attaches the test-established PTY\n 43|//! session on cold start).\n 44|//!\n...\n 63|use std::sync::atomic::{AtomicBool, Ordering};\n*64|use std::sync::{Arc, Mutex};\n 65|use std::thread;\n 66|use std::time::{Duration, Instant};\n 67|\n...\n 99|        adapter: String::new(),\n*100|        install_dir: None,\n 101|    }\n 102|}\n 103|\n...\n 117|/// endpoint for `identity`, with its effect journal under `dir`.\n*118|fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\n 119|    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\n 120|    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n 121|        .expect(\"bind broker with net\");\n*122|    let serve = Arc::clone(&broker);\n 123|    thread::spawn(move || {\n 124|        let _ = serve.serve();\n 125|    });\n...\n 172|\n*173|/// Lowercase-hex SHA-256 of a file (the same digest `brain.ready.exe_hash`\n 174|/// carries; recomputed independently here to assert the breadcrumb matches the\n 175|/// fixture the supervisor actually spawned).\n 176|fn file_hash(path: &std::path::Path) -> String {\n...\n 248|        (sid, child_pid)\n*249|        // driver dropped here — HARD detach (no graceful handoff frame).\n 250|    };\n 251|\n 252|    // ── Start the production supervisor, spawning the REAL `spt daemon brain`\n...\n 256|    let ready_path = home.path().join(\"brain.ready\");\n*257|    let stop = Arc::new(AtomicBool::new(false));\n*258|    let restart = Arc::new(BrainRestart::new());\n*259|    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260|    let home_path = home.path().to_path_buf();\n 261|\n 262|    let sup = {\n*263|        let stop = Arc::clone(&stop);\n*264|        let restart = Arc::clone(&restart);\n*265|        let selected = Arc::clone(&selected);\n 266|        thread::spawn(move || {\n 267|            supervise_brain(\n 268|                &stop,\n## composite_e2e.rs\n*1|//! THE-FORKENING W4 — the bare `spt update` composite, end to end\n 2|//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\n 3|//! release, THEN updates a registered gh_release adapter — core-first order,\n 4|//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\n...\n 13|//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\n*14|//! Windows' installer-detection heuristic forces UAC elevation on a\n 15|//! manifest-less exe whose name contains it (os error 740; the\n 16|//! post_step_fixture precedent).\n 17|//\n...\n 51|    rx.recv_timeout(deadline)\n*52|        .expect(\"spt update must complete within its deadline\")\n*53|        .expect(\"run spt update\")\n 54|}\n 55|\n 56|/// The signed update set the fake channel serves: one artifact for the CURRENT\n...\n 68|                brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n*69|                broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,\n 70|                asset_name: Some(\"spt-composite-artifact\".to_string()),\n 71|            },\n 72|        )]),\n...\n 85|\n*86|/// Build a `.spt` adapter release archive (tar, relative members — the exact\n*87|/// shape `extract_release_archive` consumes) whose root manifest declares\n 88|/// `version` and keeps the gh_release `[update]` block (so the re-register\n 89|/// after the swap preserves the avenue).\n 90|fn adapter_spt(scratch: &Path, version: &str) -> Vec<u8> {\n...\n 165|\n*166|    // ── (3) Register the gh_release adapter `cc` at 1.0.0 (its install dir is\n*167|    //    the registered source_dir — the in-place update target). ──\n 168|    std::env::set_var(\"SPT_HOME\", &home);\n*169|    let install_dir = spt_store::perch::spt_home().join(\"srcs\").join(\"cc\");\n*170|    std::fs::create_dir_all(&install_dir).unwrap();\n*171|    std::fs::write(install_dir.join(\"manifest.toml\"), adapter_manifest(\"1.0.0\")).unwrap();\n*172|    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &install_dir, 1000)\n 173|        .unwrap();\n 174|    std::env::remove_var(\"SPT_HOME\");\n 175|\n*176|    // ── (4) ONE invocation: bare `spt update`. ──\n 177|    let out = {\n\n[Showing lines 1-743 of 1108 (50.0KB limit). Use :744 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs/KNOWN-HAZARDS.md, docs/FAULT-MATRIX.md, docs/adr/0018-broker-brain-process-isolation-restoration.md, crates/spt/tests, crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":285,"fileCount":20,"files":["docs/KNOWN-HAZARDS.md","docs/FAULT-MATRIX.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/tests/adapter_post_step.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/bind_adapter_profile_persist_e2e.rs","crates/spt/tests/bind_cwd_project_e2e.rs","crates/spt/tests/bind_honest_cross_perch_e2e.rs","crates/spt/tests/bootstrap_verb_e2e.rs","crates/spt/tests/boundary_ready_strand_e2e.rs","crates/spt/tests/brain_respawn_rename.rs","crates/spt/tests/brain_survive.rs","crates/spt/tests/common/mod.rs","crates/spt/tests/composite_e2e.rs","crates/spt/tests/contract_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt/tests/drive_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/FAULT-MATRIX.md","count":6},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","count":4},{"path":"crates/spt/tests/active_only_never_relay_e2e.rs","count":5},{"path":"crates/spt/tests/adapter_post_step.rs","count":10},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":20},{"path":"crates/spt/tests/bind_adapter_profile_persist_e2e.rs","count":17},{"path":"crates/spt/tests/bind_cwd_project_e2e.rs","count":20},{"path":"crates/spt/tests/bind_honest_cross_perch_e2e.rs","count":20},{"path":"crates/spt/tests/bootstrap_verb_e2e.rs","count":19},{"path":"crates/spt/tests/boundary_ready_strand_e2e.rs","count":20},{"path":"crates/spt/tests/brain_respawn_rename.rs","count":6},{"path":"crates/spt/tests/brain_survive.rs","count":14},{"path":"crates/spt/tests/common/mod.rs","count":1},{"path":"crates/spt/tests/composite_e2e.rs","count":19},{"path":"crates/spt/tests/contract_e2e.rs","count":20},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":20},{"path":"crates/spt/tests/docs_bundle_e2e.rs","count":4},{"path":"crates/spt/tests/drive_e2e.rs","count":20},{"path":"crates/spt/tests/dummy_harness_e2e.rs","count":20}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## KNOWN-HAZARDS.md\n  4│\n *5│**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...\n  6│\n  7│---\n  8│\n   │...\n 14│- **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*15│- **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 16│\n 17│### 1.2 Poll-rewrite race & info.json mid-write reads\n 18│- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n   │...\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 26│- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n*27│- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n 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   │...\n 32│- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n*33│- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\n 34│\n*35│### 1.5 Worker (working-perch) lifecycle path consistency\n*36│- **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*37│- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\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 46│- **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*47│- **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 48│- **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*49│- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n 50│\n 51│### 2.2 Stdin session_id precedence over env\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   │...\n 60│- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n*61│- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n 62│\n 63│### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n 64│- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n   │...\n 67│- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n*68│- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n 69│\n 70│### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n*71│- **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 `...\n*72│- **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** (`inf...\n*73│- **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 ...\n*74│- **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 75│\n 76│---\n 77│\n   │...\n 79│\n*80│### 3.1 Ephemeral perch cleanup on every `ring` exit path\n*81│- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\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 32│| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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   │...\n 42│| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psy...\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   │...\n 54│|---|---|---|---|---|\n*55│| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n 56│| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57│| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58│| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n 59│| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60│| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61│| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n\n## adr/\n### 0018-broker-brain-process-isolation-restoration.md\n 10│\n*11│On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12│\n 13│Consequences of the drift:\n 14│- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n*15│- `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 16│- 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 17│- **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 18│\n   │...\n 20│\n*21│**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 22│\n 23│1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n 24│2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25│3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n*26│4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27│5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n 28│6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29│7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n\n# crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 10│//! and does NOT `wan_send`. This gate locks that: with the target resolvable ONLY on\n*11│//! a REMOTE node (a WAN snapshot, no local perch) so a `default` send WOULD take the\n 12│//! WAN leg, an `--active-only` send must refuse local-only and never touch WAN.\n 13│//!\n 14│//! Daemon-free: the active_only path returns before any broker interaction; the\n   │...\n 27│use spt_net::net::registry::{Instance, Status, SubnetRegistry};\n*28│use spt_store::perch;\n 29│\n 30│// The body does in-proc SubnetStore/nodeid/snapshot writes keyed off the\n 31│// process-global SPT_HOME — serialize so a future sibling can't race the env.\n   │...\n 60│/// Seed a subnet + a WAN registry snapshot placing `id` on a REMOTE node — so a\n*61│/// `default` send resolves it to the WAN leg (there is no local perch).\n 62│fn seed_remote_only(subnet: &str, id: &str) {\n 63│    let mut store = spt_store::subnet::SubnetStore::load();\n 64│    store.create_subnet(subnet).expect(\"seed subnet\");\n   │...\n 66│\n*67│    let dir = perch::identity_dir().join(\"registry\");\n 68│    std::fs::create_dir_all(&dir).unwrap();\n 69│    let mut reg = SubnetRegistry::new();\n 70│    reg.merge_instance(\n   │...\n 76│            epoch: 1,\n*77│            resources: None,\n 78│            last_active_ms: None,\n 79│            shell_adapters: Vec::new(),\n 80│            node_label: None,\n## adapter_post_step.rs\n  5│//!\n *6│//! The GitHub latest-version query is short-circuited by the `SPT_TEST_GH_LATEST`\n *7│//! seam set to the installed version, so the adapter is a version NO-OP — proving\n *8│//! the post-step runs UNCONDITIONALLY (even with no pull). Each mode exercises one\n  9│//! arm of the stdout notice arbitration: custom text SUPERSEDES `[update].message`;\n 10│//! the reserved sentinel FIRES the static `[update].message`; empty = no notice; a\n 11│//! nonzero exit is loud (overall exit nonzero) and failure-isolated (no rollback —\n   │...\n 20│\n*21│use spt_store::perch;\n 22│\n 23│fn sibling_bin(name: &str) -> PathBuf {\n 24│    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n   │...\n 32│        .env(\"SPT_HOME\", home)\n*33│        // No-op: latest == installed → the pull is skipped, the post-step still runs.\n 34│        .env(\"SPT_TEST_GH_LATEST\", \"1.0.0\")\n 35│        .env(\"POST_FIXTURE_MODE\", mode)\n 36│        .env(\"POST_FIXTURE_SEAM_OUT\", seam_out)\n   │...\n 53│    std::env::set_var(\"SPT_HOME\", home.path());\n*54│    let adapters = perch::adapters_dir();\n 55│\n 56│    // A gh_release adapter declaring [update.post] (the fixture) AND a static\n 57│    // [update].message (so the sentinel arm has something to fire).\n   │...\n 64│    );\n*65│    let src = perch::spt_home().join(\"srcs\").join(\"cc\");\n*66│    std::fs::create_dir_all(&src).unwrap();\n*67│    std::fs::write(src.join(\"manifest.toml\"), &manifest).unwrap();\n*68│    spt_runtime::registry::register(&adapters, &src, 1).unwrap();\n 69│    std::env::remove_var(\"SPT_HOME\");\n 70│\n 71│    let seam_out = home.path().join(\"seam.json\");\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  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  21│//! residual loopback write. This test is the faithful real-daemon proof that the wedge\n  22│//! does not occur — and folds in L3 (dead endpoint offlined within one\n    │...\n  35│use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};\n *36│use spt_store::perch::{self, ParentHint};\n  37│\n  38│fn kill_pid(pid: u32) {\n  39│    #[cfg(windows)]\n    │...\n 100│\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*106│        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);\n 107│        if now.as_deref() == Some(want) {\n 108│            return true;\n 109│        }\n    │...\n 114│\n*115│/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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 125│        .env(\"SPT_HOME\", home)\n 126│        .stdout(Stdio::piped())\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*132│    let mut rc_out = rc.stdout.take().unwrap();\n 133│    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 134│    let reader = std::thread::spawn(move || {\n 135│        let mut buf = [0u8; 4096];\n    │...\n 137│        loop {\n*138│            match rc_out.read(&mut buf) {\n 139│                Ok(0) => break,\n 140│                Ok(n) => {\n 141│                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\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## bind_adapter_profile_persist_e2e.rs\n  14│//! This drives the REAL bind path — `spt api bind`, which routes through\n *15│//! `establish_perch` → `stamp_creation_fields` — over the REAL `spt` binary (`spt` is\n *16│//! bin-only, so the binary IS the only entry to the real bind path). A perch\n  17│//! pre-stamped `<parent>:<profile>` is re-bound with an incoming BARE `<parent>`\n  18│//! (`api --adapter <parent>`, threaded verbatim to `cmd_bind`) — exactly the value\n  19│//! the agnostic hook-bind resolves; passing it explicitly reproduces the same incoming\n    │...\n  34│use spt_store::info::{self, InfoJson};\n *35│use spt_store::perch::{self, ParentHint};\n  36│\n  37│fn start_inproc_daemon() {\n *38│    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n  39│    let name = spt_daemon::seed_socket_name();\n  40│    std::thread::spawn(move || {\n  41│        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n    │...\n  61│\n *62│/// Create a top-level LIVE_AGENT perch pinned to `session_id` with a pre-stamped\n  63│/// composite `adapter` — the \"created with `<parent>:<profile>`\" starting state.\n  64│/// Owner pid = THIS process (alive): a re-bind under the SAME session id is a\n  65│/// reconnect (not a cross-session conflict), so the bind proceeds and re-stamps.\n *66│fn seed_perch_with_adapter(id: &str, session_id: &str, adapter: &str) {\n *67│    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n  68│    std::fs::create_dir_all(&path).unwrap();\n  69│    let mut rec = InfoJson::new(\n  70│        id,\n    │...\n  80│fn recorded_adapter(id: &str) -> Option<String> {\n *81│    info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))\n  82│        .expect(\"info present\")\n  83│        .adapter\n  84│}\n    │...\n 101│    std::fs::write(&manifest_path, manifest_toml).unwrap();\n*102│    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*103│    std::fs::create_dir_all(&src).unwrap();\n*104│    std::fs::write(src.join(\"manifest.toml\"), manifest_toml).unwrap();\n*105│    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 106│    let mp = manifest_path.to_string_lossy().to_string();\n 107│\n*108│    // A perch CREATED with a composite adapter `<parent>:<profile>`.\n 109│    let id = \"prof-ep\";\n 110│    let sid = \"prof-ep-sid\";\n 111│    let created_adapter = \"dummyharness:fast\";\n 112│    let parent_only = \"dummyharness\"; // what the agnostic hook-bind resolves.\n*113│    seed_perch_with_adapter(id, sid, created_adapter);\n 114│\n*115│    // Precondition: the perch starts stamped with the FULL composite option.\n 116│    assert_eq!(\n 117│        recorded_adapter(id).as_deref(),\n 118│        Some(created_adapter),\n*119│        \"PRECONDITION: the perch must start stamped with the composite adapter\"\n 120│    );\n 121│\n 122│    // THE RE-BIND, via the REAL bind path: `api bind` presenting the SAME session id\n 123│    // (a reconnect, not a conflict) with an incoming BARE `<parent>` adapter — exactly\n*124│    // what the agnostic hook-bind resolves. `bind` routes establish_perch →\n 125│    // stamp_creation_fields; the profile-preserving precedence must KEEP the composite.\n 126│    let out = {\n 127│        let mut cmd = Command::new(&spt_bin);\n## bind_cwd_project_e2e.rs\n   8│//! `dummy_harness_e2e.rs` — `spt endpoint run --start` → broker PTY → the dummy\n  *9│//! harness shells `spt api bind` — and then asserts the live perch records a\n  10│//! non-empty `info.cwd`, and that the cwd-derived project membership (the exact\n  11│//! `project_id_for_dir` + origin-union rule the picker's `local_rows` runs)\n  12│//! places the endpoint under its project.\n    │...\n  18│//! The pure union rule is unit-tested in `picker/data.rs`\n *19│//! (`origin_project_unions_into_membership`); this test proves the REAL perch\n  20│//! carries the cwd that rule consumes.\n  21│//!\n  22│//! Bounded; all spawned pids reaped SCOPED (never machine-wide — shared runner).\n    │...\n  30│\n *31│use spt_store::perch::{self, ParentHint};\n  32│\n  33│fn kill_pid(pid: u32) {\n  34│    #[cfg(windows)]\n    │...\n  93│// [int->REQ-HAZARD-BIND-CWD-UNSET] a REAL spt-hosted endpoint-run dummy-harness\n *94│// perch records a non-empty `info.cwd` (was NEVER set pre-W3 — the refuted P1),\n  95│// and the cwd-derived project membership (the picker's data path) places the\n  96│// endpoint under its project. The fresh endpoint, with no committed context,\n  97│// appears under its origin project SOLELY because of the recorded cwd.\n    │...\n 119│    //    dummy_harness_e2e.rs: a long-lived live_agent fixture that binds its\n*120│    //    perch + heartbeats, declaring [session.psyche_init] so cmd_bind marks\n 121│    //    it online). ──\n 122│    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\n 123│    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\n    │...\n 127│    );\n*128│    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*129│    std::fs::create_dir_all(&src).unwrap();\n*130│    let psyche_bin = src.join(format!(\"psychebin{exe_suffix}\"));\n 131│    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n 132│    let drops = home.path().join(\"drops\");\n 133│    std::fs::create_dir_all(&drops).unwrap();\n    │...\n 140│    );\n*141│    std::fs::write(src.join(\"manifest.toml\"), &manifest_toml).unwrap();\n*142│    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 143│\n 144│    // ── (3) The REAL daemon broker process. The broker inherits THIS process's\n 145│    //    cwd; the W1 endpoint-run shortcut launches the harness with cwd=None\n    │...\n 181│\n*182│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n*183│    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\n 184│\n*185│    // Wait for the dummy to bind its perch ONLINE (proves the bind ran — and a\n 186│    // bind is where cwd is stamped).\n 187│    let mut online = false;\n 188│    let deadline = Instant::now() + Duration::from_secs(20);\n    │...\n 196│\n*197│    // Read the bound perch's recorded cwd (the W3 field).\n*198│    let info = spt_store::info::read_info(&self_perch);\n 199│    let recorded_cwd = info.as_ref().and_then(|i| i.cwd.clone());\n 200│\n 201│    // The harness pid (long-lived PTY child) — reap target.\n    │...\n 223│    }\n*224│    let psyche_perch =\n*225│        perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\n*226│    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\n 227│        kill_pid(p);\n 228│    }\n 229│    let _ = {\n    │...\n 244│        online,\n*245│        \"the dummy harness must bind its perch ONLINE (the bind is where cwd is stamped)\"\n 246│    );\n 247│\n*248│    // The real-fixture gate: a freshly bound perch records a non-empty cwd.\n 249│    let cwd = recorded_cwd.expect(\n## bind_honest_cross_perch_e2e.rs\n   2│//! contamination is structurally impossible: the dead-owner re-pin rescue REFUSES\n  *3│//! to rebind a perch when the incoming session id AFFIRMATIVELY owns ANOTHER perch.\n   4│//!\n  *5│//! The field contamination (doyle RCA 2026-07-03): a nested psyche's SessionStart\n  *6│//! hook mis-resolved its \"self\" id to a FOREIGN, DEAD top-level perch (hall-a, whose\n   7│//! harness had exited) and fired an id-scoped `api` call presenting the PSYCHE's own\n   8│//! session id. The dead-owner rescue (REQ-HAZARD-SESSION-PIN-WEDGE) then re-pinned\n   9│//! hall-a's `info.json.session_id` to the psyche's session (359d7bd7) — the observed\n *10│//! corruption. The fix (`auth.rs` `sid_owns_another_perch`): a re-pin whose incoming\n *11│//! sid has an HONEST HOME on another perch is a cross-perch mis-stamp, never a\n  12│//! same-session reconnect — so it is refused and the victim's pin is untouched.\n  13│//!\n  14│//! This drives the REAL auth path — `spt api presence`, which routes through\n *15│//! `gated()` → `authenticate()` — over the REAL `spt` binary against a REAL detached\n  16│//! `spt daemon run`. `spt` is bin-only (no lib), so the binary IS the only entry to\n *17│//! the real auth path; there is no mock. TWO perches: victim A (dead owner) and the\n  18│//! session's honest home B. An `api presence --id A --session-id <B's sid>` must be\n  19│//! REFUSED and leave A's pin untouched (no contamination stamp lands on A).\n  20│//!\n    │...\n  30│use spt_store::info::{self, InfoJson};\n *31│use spt_store::perch::{self, ParentHint};\n  32│\n  33│/// A DEFINITELY-DEAD pid (above Linux pid_max and Windows ranges; positive-as-i32\n  34│/// so Unix kill-group semantics don't misfire). Mirrors the auth unit's DEAD_PID —\n    │...\n  77│\n *78│/// Create a top-level perch pinned to `session_id`, recording owner `pid`.\n *79│fn seed_perch(id: &str, session_id: &str, pid: u32, state: &str) -> PathBuf {\n *80│    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n  81│    std::fs::create_dir_all(&path).unwrap();\n  82│    info::write_info(&path, &InfoJson::new(id, \"2026-06-01T00:00:00Z\", pid, session_id, state)).unwrap();\n  83│    path\n    │...\n  90│#[test]\n *91│fn cross_perch_dead_owner_repin_is_refused_no_contamination() {\n  92│    // ── (1) Isolated, NET-LESS SPT_HOME (auth is pure filesystem; no net needed). ──\n  93│    let home = tempfile::tempdir().unwrap();\n  94│    std::env::set_var(\"SPT_HOME\", home.path());\n    │...\n 100│\n*101│    // ── (2) TWO perches. ──\n*102│    // Victim A: a foreign top-level LIVE_AGENT perch whose owner pid is DEAD (its\n*103│    // harness exited) — the perch the dead-owner rescue would otherwise re-pin.\n 104│    let victim_id = \"hall-a\";\n 105│    let victim_sid = \"hall-a-own-sid\";\n*106│    let victim = seed_perch(victim_id, victim_sid, DEAD_PID, \"live_agent\");\n*107│    // The session's HONEST HOME B: a perch the incoming sid AFFIRMATIVELY owns (the\n 108│    // nested psyche that legitimately holds 359d7bd7 in the field). Its liveness is\n 109│    // irrelevant to the guard — what matters is that the sid has a home elsewhere.\n 110│    let foreign_sid = \"359d7bd7\";\n*111│    let owner_b = seed_perch(\"sess-owner-b\", foreign_sid, std::process::id(), \"psyche\");\n 112│\n 113│    // ── (3) The REAL daemon broker process (doyle: real daemon; also keeps\n 114│    //    `ensure_daemon` on the api call a no-op so no stray daemon leaks). ──\n    │...\n 146│        foreign_sid,\n*147│        \"PRECONDITION: the honest-home perch must own the incoming sid\"\n 148│    );\n 149│\n 150│    // ── (4) THE ATTACK, via the REAL auth path: an id-scoped `api presence --id A`\n    │...\n 152│    //    psyche hook fired. `presence` routes gated() → authenticate(); the dead-owner\n*153│    //    rescue must REFUSE because the sid owns another perch (bind honesty). ──\n 154│    let attack = {\n 155│        let mut cmd = Command::new(&spt_bin);\n 156│        cmd.no_window()\n## bootstrap_verb_e2e.rs\n *1│//! THE-FORKENING W1 — the self-install verb, driven end to end as a virgin\n  2│//! box would use it (ADR-0036 §3, bootstrap path b): the release binary is\n *3│//! \"downloaded\" under its `spt-*` asset name (never an installer-detection\n *4│//! trigger word), runs `install` against a clean simulated `SPT_HOME`, lands\n  5│//! at the canonical `<SPT_HOME>/bin` path, is idempotent on re-run, and the\n *6│//! PLACED binary's `spt update fetch` speaks the gh channel — proven by the\n  7│//! loud `GhCliRequired` refusal when no `gh` is resolvable, which is the gh\n  8│//! carrier gate itself, not an HTTPS fallback.\n  9│//!\n   │...\n 12│//!\n*13│//! Named `bootstrap_verb_e2e`, NOT `install_verb_e2e`: an exe whose NAME\n*14│//! carries install/setup/update trips Windows installer-detection (UAC os\n 15│//! error 740) — the test binary itself proved the gotcha the verb's `spt-*`\n 16│//! asset-name rule exists to dodge.\n 17│//\n*18│// [int->REQ-INSTALL-BOOTSTRAP-VERB]\n 19│\n 20│use std::process::Command;\n 21│\n   │...\n 30│\n*31│fn installed_name() -> &'static str {\n 32│    if cfg!(windows) {\n 33│        \"spt.exe\"\n 34│    } else {\n   │...\n 39│#[test]\n*40│fn downloaded_binary_self_installs_and_speaks_gh_channel() {\n 41│    let tmp = tempfile::tempdir().expect(\"tempdir\");\n 42│    let home = tmp.path().join(\"spt-home\");\n 43│    std::fs::create_dir_all(&home).unwrap();\n   │...\n 48│\n*49│    // 1. Self-install into the clean home (default dir = <SPT_HOME>/bin).\n 50│    let out = Command::new(&downloaded)\n*51│        .args([\"install\", \"--no-path\"])\n 52│        .env(\"SPT_HOME\", &home)\n 53│        .output()\n*54│        .expect(\"run install\");\n 55│    let stdout = String::from_utf8_lossy(&out.stdout);\n 56│    assert!(\n 57│        out.status.success(),\n*58│        \"install failed: {stdout} {}\",\n 59│        String::from_utf8_lossy(&out.stderr)\n 60│    );\n*61│    let target = home.join(\"bin\").join(installed_name());\n 62│    assert!(target.is_file(), \"binary placed at the canonical path\");\n*63│    assert!(stdout.contains(\"INSTALL_OK\"), \"{stdout}\");\n 64│\n 65│    // 2. Idempotent re-run — still green, same target.\n 66│    let out2 = Command::new(&downloaded)\n*67│        .args([\"install\", \"--no-path\"])\n 68│        .env(\"SPT_HOME\", &home)\n 69│        .output()\n*70│        .expect(\"re-run install\");\n 71│    assert!(\n 72│        out2.status.success(),\n 73│        \"re-run must stay green: {}\",\n   │...\n 77│    // 3. The PLACED binary works and its update fetch speaks the gh channel:\n*78│    //    with no resolvable gh the carrier gate refuses LOUD with the install\n 79│    //    hint — the gh mandate itself, before any network or key work.\n 80│    let empty_path = tmp.path().join(\"empty-path-dir\");\n 81│    std::fs::create_dir_all(&empty_path).unwrap();\n   │...\n 86│        .output()\n*87│        .expect(\"run placed spt update fetch\");\n 88│    let stderr = String::from_utf8_lossy(&fetch.stderr);\n 89│    assert!(!fetch.status.success(), \"fetch without gh must refuse\");\n 90│    assert!(\n## boundary_ready_strand_e2e.rs\n   1│//! F-029 C-2 (REQ-HAZARD-BOUNDARY-READY-STRAND) — SEAM-2, B6's second half. A\n  *2│//! `/clear` boundary must LEAVE the perch a live inject target: the pre-boundary\n   3│//! soft `api session-end` (which CC fires for the DEPARTING session before\n   4│//! SessionStart, while its sid still matches the pin) removes the ready marker; the\n  *5│//! boundary must re-stamp it, or every post-clear force-native (incl. the checkpoint\n   6│//! FIRE) reports the generic UNDELIVERED forever (is_online false → the CLI inject\n   7│//! gate Nones before any broker RPC).\n   8│//!\n   9│//! This drives the REAL boundary path over the REAL `spt` binary: a bound,\n *10│//! controllable, online perch → real soft `api session-end` (ready gone,\n  11│//! is_spt_hosted_no_relay false) → real `api boundary clear` → ready PRESENT +\n  12│//! is_spt_hosted_no_relay TRUE again. RED on 0.23.0 (the boundary leaves ready\n  13│//! absent). Any auto-started daemon against the throwaway home is reaped scoped.\n    │...\n  23│use spt_store::liveness::STATUS_ONLINE;\n *24│use spt_store::perch::{self, ParentHint};\n  25│\n  26│fn start_inproc_daemon() {\n *27│    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n  28│    let name = spt_daemon::seed_socket_name();\n  29│    std::thread::spawn(move || {\n  30│        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n    │...\n  50│\n *51│/// Seed a fully BOUND spt-hosted perch: online + controllable + ready marker +\n  52│/// live owner pid (this process) — an `is_spt_hosted_no_relay` inject target.\n *53│fn seed_bound_perch(id: &str, session_id: &str) {\n *54│    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n  55│    std::fs::create_dir_all(&path).unwrap();\n  56│    let mut rec = InfoJson::new(\n  57│        id,\n    │...\n  65│    info::write_info(&path, &rec).unwrap();\n *66│    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n  67│}\n  68│\n  69│#[test]\n    │...\n  81│    std::fs::write(&manifest_path, manifest_toml).unwrap();\n *82│    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n *83│    std::fs::create_dir_all(&src).unwrap();\n *84│    std::fs::write(src.join(\"manifest.toml\"), manifest_toml).unwrap();\n *85│    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n  86│    let mp = manifest_path.to_string_lossy().to_string();\n  87│\n  88│    let id = \"clr-ep\";\n    │...\n  90│    let new_sid = \"clr-ep-sid-2\";\n *91│    seed_bound_perch(id, sid);\n  92│\n *93│    let owlery = perch::owlery_dir();\n *94│    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n  95│    let spt = |args: &[&str]| {\n  96│        let mut cmd = Command::new(&spt_bin);\n  97│        cmd.no_window()\n    │...\n 103│\n*104│    // Precondition: a bound perch is a live inject target.\n 105│    assert!(\n 106│        spt_daemon::is_spt_hosted_no_relay(id, &owlery),\n*107│        \"PRECONDITION: a bound online+controllable+ready perch is an inject target\"\n 108│    );\n 109│\n 110│    // ── The pre-boundary soft session-end (CC fires it for the departing session,\n    │...\n 117│        !spt_daemon::is_spt_hosted_no_relay(id, &owlery),\n*118│        \"with ready gone the perch is NO LONGER an inject target (the SEAM-2 strand)\"\n 119│    );\n 120│\n 121│    // ── THE BOUNDARY: rotates the sid AND (C-2) re-stamps ready. REAL `api boundary\n    │...\n 130│    let target_after = spt_daemon::is_spt_hosted_no_relay(id, &owlery);\n*131│    let sid_after = info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))\n 132│        .map(|r| r.session_id);\n 133│    eprintln!(\n 134│        \"=== F029 C-2 DIAGNOSTIC: boundary_exit={:?} ready_after={ready_after} \\\n## brain_respawn_rename.rs\n   7│//! distinct fixtures through an injected spawn closure — it never renames a\n  *8│//! *running* binary, so it cannot exercise the production respawn resolution\n   9│//! (`std::env::current_exe()` in the resident broker). That gap let the v0.4.1\n  10│//! Linux brain-respawn bug ship green: on Linux `current_exe()` is\n  11│//! `readlink(/proc/self/exe)`, **inode-tracking**, so it FOLLOWS the `apply`\n    │...\n  64│\n *65│/// Lowercase-hex SHA-256 of a file — the digest `brain.ready.exe_hash` carries.\n  66│fn file_hash(path: &Path) -> String {\n  67│    use sha2::{Digest, Sha256};\n  68│    let bytes = std::fs::read(path).expect(\"read file for hashing\");\n    │...\n  94│\n *95│    // The canonical install path P the broker runs from — so the broker's own\n  96│    // current_exe IS P (the resident-broker condition the bug needs).\n  97│    let canonical = bindir.join(format!(\"spt{suffix}\"));\n *98│    std::fs::copy(&real, &canonical).expect(\"install bytes A at canonical P\");\n  99│    let hash_a = file_hash(&canonical);\n 100│\n 101│    // The REAL broker process, launched FROM P.\n    │...\n 117│        Some(hash_a.as_str()),\n*118│        \"gen-0 brain runs bytes A (the canonical install)\"\n 119│    );\n 120│\n 121│    // ── The real apply choreography on disk: rename the running canonical binary\n 122│    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\n*123│    //    what `spt update apply` does (rename-then-replace). ──\n 124│    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\n 125│    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\n 126│    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\n## brain_survive.rs\n  20│//!   in-process [`Broker`] runs the **same Broker code holding the same\n *21│//!   resources** the real broker would across a brain swap. The claim under test\n  22│//!   is the brain process + the swapped binary + broker-held endpoint survival —\n  23│//!   all real here. (Bonus: the in-process broker gives the test direct access to\n  24│//!   its session pid / count and QUIC conn count for the assertions.)\n    │...\n  40│//! `exe_hash` breadcrumb (`exe_hash == fixture-B != fixture-A`). It is also the\n *41│//! **first process-level exercise of the D4 cursor-of-record resume with N≥1\n  42│//! hosted sessions** (the supervised child re-attaches the test-established PTY\n  43│//! session on cold start).\n  44│//!\n    │...\n  63│use std::sync::atomic::{AtomicBool, Ordering};\n *64│use std::sync::{Arc, Mutex};\n  65│use std::thread;\n  66│use std::time::{Duration, Instant};\n  67│\n    │...\n  99│        adapter: String::new(),\n*100│        install_dir: None,\n 101│    }\n 102│}\n 103│\n    │...\n 117│/// endpoint for `identity`, with its effect journal under `dir`.\n*118│fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\n 119│    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\n 120│    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n 121│        .expect(\"bind broker with net\");\n*122│    let serve = Arc::clone(&broker);\n 123│    thread::spawn(move || {\n 124│        let _ = serve.serve();\n 125│    });\n    │...\n 172│\n*173│/// Lowercase-hex SHA-256 of a file (the same digest `brain.ready.exe_hash`\n 174│/// carries; recomputed independently here to assert the breadcrumb matches the\n 175│/// fixture the supervisor actually spawned).\n 176│fn file_hash(path: &std::path::Path) -> String {\n    │...\n 248│        (sid, child_pid)\n*249│        // driver dropped here — HARD detach (no graceful handoff frame).\n 250│    };\n 251│\n 252│    // ── Start the production supervisor, spawning the REAL `spt daemon brain`\n    │...\n 256│    let ready_path = home.path().join(\"brain.ready\");\n*257│    let stop = Arc::new(AtomicBool::new(false));\n*258│    let restart = Arc::new(BrainRestart::new());\n*259│    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260│    let home_path = home.path().to_path_buf();\n 261│\n 262│    let sup = {\n*263│        let stop = Arc::clone(&stop);\n*264│        let restart = Arc::clone(&restart);\n*265│        let selected = Arc::clone(&selected);\n 266│        thread::spawn(move || {\n 267│            supervise_brain(\n 268│                &stop,\n## composite_e2e.rs\n  *1│//! THE-FORKENING W4 — the bare `spt update` composite, end to end\n   2│//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\n   3│//! release, THEN updates a registered gh_release adapter — core-first order,\n   4│//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\n    │...\n  13│//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\n *14│//! Windows' installer-detection heuristic forces UAC elevation on a\n  15│//! manifest-less exe whose name contains it (os error 740; the\n  16│//! post_step_fixture precedent).\n  17│//\n    │...\n  51│    rx.recv_timeout(deadline)\n *52│        .expect(\"spt update must complete within its deadline\")\n *53│        .expect(\"run spt update\")\n  54│}\n  55│\n  56│/// The signed update set the fake channel serves: one artifact for the CURRENT\n    │...\n  68│                brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n *69│                broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,\n  70│                asset_name: Some(\"spt-composite-artifact\".to_string()),\n  71│            },\n  72│        )]),\n    │...\n  85│\n *86│/// Build a `.spt` adapter release archive (tar, relative members — the exact\n *87│/// shape `extract_release_archive` consumes) whose root manifest declares\n  88│/// `version` and keeps the gh_release `[update]` block (so the re-register\n  89│/// after the swap preserves the avenue).\n  90│fn adapter_spt(scratch: &Path, version: &str) -> Vec<u8> {\n    │...\n 165│\n*166│    // ── (3) Register the gh_release adapter `cc` at 1.0.0 (its install dir is\n*167│    //    the registered source_dir — the in-place update target). ──\n 168│    std::env::set_var(\"SPT_HOME\", &home);\n*169│    let install_dir = spt_store::perch::spt_home().join(\"srcs\").join(\"cc\");\n*170│    std::fs::create_dir_all(&install_dir).unwrap();\n*171│    std::fs::write(install_dir.join(\"manifest.toml\"), adapter_manifest(\"1.0.0\")).unwrap();\n*172│    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &install_dir, 1000)\n 173│        .unwrap();\n 174│    std::env::remove_var(\"SPT_HOME\");\n 175│\n*176│    // ── (4) ONE invocation: bare `spt update`. ──\n 177│    let out = {\n 178│        let mut cmd = Command::new(&spt);\n 179│        cmd.no_window()\n    │...\n 196│        out.status.success(),\n*197│        \"bare `spt update` must exit 0 when both legs succeed\"\n 198│    );\n 199│\n 200│    // ── (5) CORE leg landed: the private spt copy now holds the NEW bytes\n*201│    //    (daemonless swap — no daemon in this home), reported as installed. ──\n 202│    assert_eq!(\n 203│        std::fs::read(&spt).unwrap(),\n 204│        NEW_CORE_BYTES,\n    │...\n 207│    assert!(\n*208│        stdout.contains(\"Installed spt-core v9.9.9 on disk\"),\n 209│        \"the daemonless applied notice names the product version: {stdout}\"\n 210│    );\n 211│\n    │...\n 221│    );\n*222│    let landed = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).unwrap();\n 223│    assert!(\n 224│        landed.contains(\"version = \\\"1.1.0\\\"\"),\n*225│        \"the registered install dir must hold the new manifest: {landed}\"\n 226│    );\n 227│\n 228│    // ── (7) CORE-FIRST order (the composite doctrine): the set download\n## contract_e2e.rs\n   5│//! helper → the helper fires `spt api bind` (skeleton→live) then\n  *6│//! `spt api state idle` → a message is spooled to the perch → `spt api poll`\n   7│//! drains it. Asserts the round-trip.\n   8│//!\n  *9│//! A second test exercises the **harness-hosted startup** topology proper —\n  10│//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered\n  11│//! message — the REQ-START-2 path.\n  12│//\n    │...\n  28│use spt_runtime::{AgentRuntime, Manifest, ManifestRuntime};\n *29│use spt_store::perch::{self, ParentHint};\n  30│use spt_store::spool;\n  31│\n  32│/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.\n    │...\n  50│fn start_inproc_daemon() {\n *51│    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n  52│    let name = spt_daemon::seed_socket_name();\n  53│    std::thread::spawn(move || {\n  54│        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n    │...\n 112│\n*113│    // Outbound: spawn the session. Bounded (not detached) so the test waits for\n 114│    // the helper's bind + state-idle to complete before asserting.\n 115│    let out = runtime\n 116│        .run_bounded(\n    │...\n 129│\n*130│    // Inbound assertion 1 — `api bind` flipped the perch live (REQ-START-2 /\n 131│    // REQ-SEAM-POSTSPAWN): info.json exists with the reported session id.\n*132│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*133│    let rec = spt_store::info::read_info(&perch_path).expect(\"perch info.json after bind\");\n 134│    assert_eq!(rec.session_id, session_id);\n 135│    assert_eq!(rec.state, \"live_agent\");\n 136│\n    │...\n 138│    assert!(\n*139│        perch_path.join(\".idle\").exists(),\n 140│        \"state idle must write the .idle sentinel\"\n 141│    );\n 142│\n*143│    // A peer spools a message to the now-live perch.\n*144│    spool::spool_message_at(&perch_path, \"tester\", \"hello over the contract\").unwrap();\n 145│\n 146│    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).\n 147│    let poll = Command::new(&spt_bin).no_window()\n    │...\n 167│    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH\n*168│    // exercised through the real binary).\n 169│    let unauth = Command::new(&spt_bin).no_window()\n 170│        .args([\"api\", \"--adapter\", \"mock\", \"poll\", id])\n 171│        .env(\"SPT_HOME\", home.path())\n    │...\n 181│/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed\n*182│/// (matched by parent pid, validated against pid-recycle), binds the perch\n 183│/// skeleton→live, and relays a delivered message.\n 184│#[test]\n 185│fn seed_then_listen_binds_and_relays() {\n    │...\n 195│\n*196│    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.\n 197│    std::env::set_var(\"SPT_HOME\", home.path());\n 198│    start_inproc_daemon();\n*199│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*200│    spool::spool_message_at(&perch_path, \"tester\", \"delivered via listen\").unwrap();\n 201│\n 202│    // Seed the startup record keyed by the anchor pid.\n 203│    let seed = Command::new(&spt_bin).no_window()\n    │...\n 219│    // Listen consumes the seed (explicit --parent-pid for a deterministic match),\n*220│    // binds the perch, drains the backlog, and exits (--once).\n 221│    let listen = Command::new(&spt_bin).no_window()\n 222│        .args([\n 223│            \"api\",\n    │...\n 245│\n*246│    // The perch was bound live with the seed's session id.\n*247│    let rec = spt_store::info::read_info(&perch_path).expect(\"perch info.json after listen\");\n 248│    assert_eq!(rec.session_id, session_id);\n 249│    assert_eq!(rec.state, \"live_agent\");\n 250│}\n    │...\n 253│/// the **generic mock adapter**, harness-agnostic:\n*254│/// spawn a LiveAgent → its Psyche spawns as a nested perch → a commune drop is\n 255│/// ingested + deleted by spt-core → a graceful `spt shutdown` fires the\n 256│/// echo-commune *before* teardown.\n 257│#[test]\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   6│//! verb. The recovery story: wedged brain-held state no longer needs a full\n   7│//! daemon bounce that kills every PTY.\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  22│\n *23│use spt_store::perch::{self, ParentHint};\n  24│\n  25│/// Scoped pid-tree kill (cleanup) — never machine-wide.\n  26│fn kill_pid(pid: u32) {\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 105│    //    (the live_adapt rig's manifest, minus the translation binary). ──\n*106│    let install_dir = perch::spt_home().join(\"srcs\").join(\"cc\");\n*107│    std::fs::create_dir_all(&install_dir).unwrap();\n 108│    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\n 109│    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\n 110│    let self_cmd = format!(\n    │...\n 113│    );\n*114│    let psyche_bin = install_dir.join(format!(\"psychebin{exe_suffix}\"));\n 115│    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n 116│    let drops = home.path().join(\"drops\");\n 117│    std::fs::create_dir_all(&drops).unwrap();\n    │...\n 124│    );\n*125│    std::fs::write(install_dir.join(\"manifest.toml\"), &manifest_toml).unwrap();\n*126│    spt_runtime::registry::register(&perch::adapters_dir(), &install_dir, 1000).unwrap();\n 127│\n 128│    // ── (3) The REAL daemon broker process. ──\n 129│    let brain_log = home.path().join(\"brain.stderr.log\");\n    │...\n 154│    // ── (4) Host a real endpoint: `endpoint run --adapter cc --id ccX --start`,\n*155│    //    wait for its perch ONLINE. ──\n 156│    let id = \"ccX\";\n 157│    let run = {\n 158│        let mut cmd = Command::new(&spt_bin);\n    │...\n 170│\n*171│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n*172│    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\n 173│    let mut online = false;\n 174│    let deadline = Instant::now() + Duration::from_secs(25);\n 175│    while Instant::now() < deadline {\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*191│    let refresh_stdout = String::from_utf8_lossy(&refresh.stdout).to_string();\n*192│    let refresh_stderr = String::from_utf8_lossy(&refresh.stderr).to_string();\n 193│\n 194│    // ── (6) The brain must CYCLE: a new pid stamps brain.ready with a bumped\n 195│    //    generation (no binary swapped anywhere). ──\n    │...\n 200│\n*201│    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\n 202│    let harness_alive_after = harness_pid\n 203│        .map(spt_store::proc::is_process_alive)\n 204│        .unwrap_or(false);\n## docs_bundle_e2e.rs\n   2│//! (ADR-0036 §4): a staged set whose SIGNED metadata carries the docs entry\n  *3│//! lands version-matched docs at `$SPT_HOME/docs` on `spt update apply`; a\n   4│//! corrupted staged bundle degrades to the machine-greppable\n   5│//! `UPDATE_DOCS_SKIPPED` on stderr while the BINARY apply outcome is\n   6│//! byte-for-byte unchanged (doyle pre-ruling 2026-07-14: assert both).\n    │...\n  42│                brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n *43│                broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,\n  44│                asset_name: Some(\"spt-test-artifact\".to_string()),\n  45│            },\n  46│        )]),\n    │...\n 104│/// Stage set + artifact (+ optionally docs bytes) into `home`'s cache, then\n*105│/// run the real `spt update apply` against that home. Returns (exit ok,\n 106│/// stdout, stderr, swapped binary bytes).\n 107│fn stage_and_apply(\n 108│    home: &Path,\n    │...\n 131│        .output()\n*132│        .expect(\"run spt update apply\");\n 133│    // The swap targets the RUNNING exe — read back the driven copy itself.\n 134│    let swapped = std::fs::read(spt).ok();\n 135│    (\n## drive_e2e.rs\n   4│//!\n  *5│//! spawn (offline) → `api bind-shell` onlines the perch → `spt shell drive`\n   6│//! writes the daemon's single live slot → `api drive-poll --link` takes-and-clears\n   7│//! it. The slot lives in the daemon (here an in-process drive-control serve on the\n  *8│//! home's drive socket — the broker-home anchor in production), so this exercises\n   9│//! the true CLI→socket→hub→socket→CLI wire, not a hub call.\n  10│//!\n  11│//! Proves: (1) drop-at-write-offline, (2) latest-wins overwrite, (3)\n    │...\n  13│//! clear-on-link-break (write → break → relink → poll EMPTY), (6) slot has no\n *14│//! file under the perch. The cross-node leg is the [twohost] test; this is the\n  15│//! same-node gate.\n  16│//\n  17│// [int->REQ-SHELL-3]\n    │...\n  25│\n *26│use spt_store::perch::{self, ParentHint};\n  27│\n  28│/// Stand up the in-process daemon surfaces this E2E needs on the current\n  29│/// `SPT_HOME`'s sockets: the **seed** control (so subprocess `ensure_running`\n *30│/// pings succeed without spawning a real detached daemon) and the **drive**\n  31│/// control (the broker-home slot the drive surfaces read/write). Both serve\n  32│/// threads are leaked; they die with the test process.\n  33│fn start_inproc_daemon() {\n *34│    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n  35│    let seed = spt_daemon::seed_socket_name();\n  36│    std::thread::spawn(move || {\n  37│        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);\n  38│    });\n *39│    let hub = std::sync::Arc::new(spt_daemon::DriveHub::new());\n  40│    let drive = spt_daemon::drive_socket_name();\n  41│    std::thread::spawn(move || {\n  42│        let _ = spt_daemon::serve_drive_control(&drive, hub);\n    │...\n  53│/// `Command::output()` with a deadline, run off-thread (the shell_e2e pattern —\n *54│/// a detached child inheriting the capture pipe would otherwise hang).\n  55│fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n  56│    let (tx, rx) = std::sync::mpsc::channel();\n  57│    std::thread::spawn(move || {\n    │...\n  65│fn register_adapter(name: &str, manifest: &str) {\n *66│    let src = perch::spt_home().join(\"srcs\").join(name);\n *67│    std::fs::create_dir_all(&src).unwrap();\n *68│    std::fs::write(src.join(\"manifest.toml\"), manifest).unwrap();\n *69│    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n  70│}\n  71│\n  72│#[test]\n    │...\n  86│    // A shell adapter with a [shell.drive] vocabulary. The spawn template is a\n *87│    // noop that exits immediately (never binds) — the perch stays offline until\n  88│    // we online it by token, so we control the online/offline edges precisely.\n  89│    #[cfg(windows)]\n  90│    let noop = \"cmd /c exit 0\";\n    │...\n 101│\n*102│    // The owner: a bound Self perch (the bind-by-token scan starts at Self\n*103│    // perches) + node identity.\n*104│    let owner_perch = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n*105│    std::fs::create_dir_all(&owner_perch).unwrap();\n 106│    let rec = spt_store::info::InfoJson::new(\"doyle\", \"0\", std::process::id(), \"sid\", \"live_agent\");\n*107│    spt_store::info::write_info(&owner_perch, &rec).unwrap();\n 108│    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 109│\n*110│    let owlery = perch::owlery_dir();\n*111│    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, \"doyle\", \"mock-shell-0\");\n 112│    let token_of = || {\n## dummy_harness_e2e.rs\n   3│//! REAL spt-hosted bringup path end-to-end —\n  *4│//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach\n  *5│//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and\n   6│//! NOT a mock that exits immediately.\n   7│//!\n  *8│//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the\n   9│//! harness contract), prints a stdout heartbeat on an interval, and stays alive\n  10│//! until killed — so \"broker wedged / endpoint died\" is cleanly separable from \"a\n *11│//! real harness failed to launch.\" Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,\n  12│//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.\n  13│//!\n *14│//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the\n *15│//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the\n  16│//! live heartbeat output. Bounded; all spawned pids reaped scoped (never\n  17│//! machine-wide — shared runner).\n  18│\n    │...\n  27│\n *28│use spt_store::perch::{self, ParentHint};\n  29│\n  30│// Both tests in this file set the PROCESS-GLOBAL env var SPT_HOME then do in-proc\n *31│// reads of it (perch::spt_home / adapters_dir, registry::register, psychebin\n  32│// fs::copy staging). One file = one test binary → the two tests otherwise run on\n  33│// parallel threads and their set_var/reads race (register lands in the wrong home\n  34│// → ENDPOINT_RUN_ADAPTER_UNREGISTERED; psychebin staging → os-32 sharing\n    │...\n  86│#[test]\n *87│fn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {\n  88│    // Serialize against the sister test — shared process-global SPT_HOME.\n  89│    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\n  90│    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node\n *91│    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces\n  92│    //    as net-stream events, so it requires a net-bound broker — a net-less broker\n  93│    //    refuses the loopback dial (\"net disabled\"). Unlike the livehost/reconcile\n *94│    //    tests (which only inspect perch state), the rc-attach path needs net up. ──\n  95│    let home = tempfile::tempdir().unwrap();\n  96│    std::env::set_var(\"SPT_HOME\", home.path());\n  97│\n    │...\n 126│    // resident process the reconcile hosts — no resident `{id}-psyche` is expected,\n*127│    // so the psyche-perch reap at teardown (step 6) is belt-and-suspenders.\n*128│    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*129│    std::fs::create_dir_all(&src).unwrap();\n*130│    let psyche_bin = src.join(format!(\"psychebin{exe_suffix}\"));\n 131│    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n 132│    let drops = home.path().join(\"drops\");\n 133│    std::fs::create_dir_all(&drops).unwrap();\n    │...\n 140│    );\n*141│    std::fs::write(src.join(\"manifest.toml\"), &manifest_toml).unwrap();\n*142│    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 143│\n 144│    // ── (3) The REAL daemon broker process. ──\n 145│    let brain_log = home.path().join(\"brain.stderr.log\");\n    │...\n 168│    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the\n*169│    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──\n 170│    let id = \"dummy7\";\n 171│    let run = {\n 172│        let mut cmd = Command::new(&spt_bin);\n    │...\n 178│\n*179│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n*180│    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\n 181│\n\n## common/\n### mod.rs\n 10│/// `brain` subprocess a test spawns). No-op on non-Windows. CREATE_NO_WINDOW does\n*11│/// NOT detach — env/stdio inheritance is unchanged — it only hides the console.\n 12│///\n 13│/// Usage: `Command::new(bin).no_window().args([...]).spawn()`.\n 14│pub trait CommandNoWindowExt {","missingPaths":["docs/UPDATE-WEDGE-ROOT-CAUSE.md"],"truncation":{"content":"# docs/\n## KNOWN-HAZARDS.md\n 4|\n*5|**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...\n 6|\n 7|---\n 8|\n...\n 14|- **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*15|- **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 16|\n 17|### 1.2 Poll-rewrite race & info.json mid-write reads\n 18|- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n...\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 26|- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n*27|- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n 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...\n 32|- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n*33|- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\n 34|\n*35|### 1.5 Worker (working-perch) lifecycle path consistency\n*36|- **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*37|- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\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 46|- **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*47|- **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 48|- **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*49|- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n 50|\n 51|### 2.2 Stdin session_id precedence over env\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...\n 60|- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n*61|- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n 62|\n 63|### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n 64|- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n...\n 67|- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n*68|- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n 69|\n 70|### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n*71|- **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 `...\n*72|- **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** (`inf...\n*73|- **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 ...\n*74|- **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 75|\n 76|---\n 77|\n...\n 79|\n*80|### 3.1 Ephemeral perch cleanup on every `ring` exit path\n*81|- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\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 32|| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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...\n 42|| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psy...\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...\n 54||---|---|---|---|---|\n*55|| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n 56|| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57|| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58|| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n 59|| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60|| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61|| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n\n## adr/\n### 0018-broker-brain-process-isolation-restoration.md\n 10|\n*11|On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12|\n 13|Consequences of the drift:\n 14|- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n*15|- `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 16|- 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 17|- **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 18|\n...\n 20|\n*21|**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 22|\n 23|1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n 24|2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25|3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n*26|4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27|5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n 28|6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29|7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n\n# crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 10|//! and does NOT `wan_send`. This gate locks that: with the target resolvable ONLY on\n*11|//! a REMOTE node (a WAN snapshot, no local perch) so a `default` send WOULD take the\n 12|//! WAN leg, an `--active-only` send must refuse local-only and never touch WAN.\n 13|//!\n 14|//! Daemon-free: the active_only path returns before any broker interaction; the\n...\n 27|use spt_net::net::registry::{Instance, Status, SubnetRegistry};\n*28|use spt_store::perch;\n 29|\n 30|// The body does in-proc SubnetStore/nodeid/snapshot writes keyed off the\n 31|// process-global SPT_HOME — serialize so a future sibling can't race the env.\n...\n 60|/// Seed a subnet + a WAN registry snapshot placing `id` on a REMOTE node — so a\n*61|/// `default` send resolves it to the WAN leg (there is no local perch).\n 62|fn seed_remote_only(subnet: &str, id: &str) {\n 63|    let mut store = spt_store::subnet::SubnetStore::load();\n 64|    store.create_subnet(subnet).expect(\"seed subnet\");\n...\n 66|\n*67|    let dir = perch::identity_dir().join(\"registry\");\n 68|    std::fs::create_dir_all(&dir).unwrap();\n 69|    let mut reg = SubnetRegistry::new();\n 70|    reg.merge_instance(\n...\n 76|            epoch: 1,\n*77|            resources: None,\n 78|            last_active_ms: None,\n 79|            shell_adapters: Vec::new(),\n 80|            node_label: None,\n## adapter_post_step.rs\n 5|//!\n*6|//! The GitHub latest-version query is short-circuited by the `SPT_TEST_GH_LATEST`\n*7|//! seam set to the installed version, so the adapter is a version NO-OP — proving\n*8|//! the post-step runs UNCONDITIONALLY (even with no pull). Each mode exercises one\n 9|//! arm of the stdout notice arbitration: custom text SUPERSEDES `[update].message`;\n 10|//! the reserved sentinel FIRES the static `[update].message`; empty = no notice; a\n 11|//! nonzero exit is loud (overall exit nonzero) and failure-isolated (no rollback —\n...\n 20|\n*21|use spt_store::perch;\n 22|\n 23|fn sibling_bin(name: &str) -> PathBuf {\n 24|    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n...\n 32|        .env(\"SPT_HOME\", home)\n*33|        // No-op: latest == installed → the pull is skipped, the post-step still runs.\n 34|        .env(\"SPT_TEST_GH_LATEST\", \"1.0.0\")\n 35|        .env(\"POST_FIXTURE_MODE\", mode)\n 36|        .env(\"POST_FIXTURE_SEAM_OUT\", seam_out)\n...\n 53|    std::env::set_var(\"SPT_HOME\", home.path());\n*54|    let adapters = perch::adapters_dir();\n 55|\n 56|    // A gh_release adapter declaring [update.post] (the fixture) AND a static\n 57|    // [update].message (so the sentinel arm has something to fire).\n...\n 64|    );\n*65|    let src = perch::spt_home().join(\"srcs\").join(\"cc\");\n*66|    std::fs::create_dir_all(&src).unwrap();\n*67|    std::fs::write(src.join(\"manifest.toml\"), &manifest).unwrap();\n*68|    spt_runtime::registry::register(&adapters, &src, 1).unwrap();\n 69|    std::env::remove_var(\"SPT_HOME\");\n 70|\n 71|    let seam_out = home.path().join(\"seam.json\");\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 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 21|//! residual loopback write. This test is the faithful real-daemon proof that the wedge\n 22|//! does not occur — and folds in L3 (dead endpoint offlined within one\n...\n 35|use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};\n*36|use spt_store::perch::{self, ParentHint};\n 37|\n 38|fn kill_pid(pid: u32) {\n 39|    #[cfg(windows)]\n...\n 100|\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*106|        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);\n 107|        if now.as_deref() == Some(want) {\n 108|            return true;\n 109|        }\n...\n 114|\n*115|/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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 125|        .env(\"SPT_HOME\", home)\n 126|        .stdout(Stdio::piped())\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*132|    let mut rc_out = rc.stdout.take().unwrap();\n 133|    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 134|    let reader = std::thread::spawn(move || {\n 135|        let mut buf = [0u8; 4096];\n...\n 137|        loop {\n*138|            match rc_out.read(&mut buf) {\n 139|                Ok(0) => break,\n 140|                Ok(n) => {\n 141|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\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## bind_adapter_profile_persist_e2e.rs\n 14|//! This drives the REAL bind path — `spt api bind`, which routes through\n*15|//! `establish_perch` → `stamp_creation_fields` — over the REAL `spt` binary (`spt` is\n*16|//! bin-only, so the binary IS the only entry to the real bind path). A perch\n 17|//! pre-stamped `<parent>:<profile>` is re-bound with an incoming BARE `<parent>`\n 18|//! (`api --adapter <parent>`, threaded verbatim to `cmd_bind`) — exactly the value\n 19|//! the agnostic hook-bind resolves; passing it explicitly reproduces the same incoming\n...\n 34|use spt_store::info::{self, InfoJson};\n*35|use spt_store::perch::{self, ParentHint};\n 36|\n 37|fn start_inproc_daemon() {\n*38|    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n 39|    let name = spt_daemon::seed_socket_name();\n 40|    std::thread::spawn(move || {\n 41|        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n...\n 61|\n*62|/// Create a top-level LIVE_AGENT perch pinned to `session_id` with a pre-stamped\n 63|/// composite `adapter` — the \"created with `<parent>:<profile>`\" starting state.\n 64|/// Owner pid = THIS process (alive): a re-bind under the SAME session id is a\n 65|/// reconnect (not a cross-session conflict), so the bind proceeds and re-stamps.\n*66|fn seed_perch_with_adapter(id: &str, session_id: &str, adapter: &str) {\n*67|    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n 68|    std::fs::create_dir_all(&path).unwrap();\n 69|    let mut rec = InfoJson::new(\n 70|        id,\n...\n 80|fn recorded_adapter(id: &str) -> Option<String> {\n*81|    info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))\n 82|        .expect(\"info present\")\n 83|        .adapter\n 84|}\n...\n 101|    std::fs::write(&manifest_path, manifest_toml).unwrap();\n*102|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*103|    std::fs::create_dir_all(&src).unwrap();\n*104|    std::fs::write(src.join(\"manifest.toml\"), manifest_toml).unwrap();\n*105|    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 106|    let mp = manifest_path.to_string_lossy().to_string();\n 107|\n*108|    // A perch CREATED with a composite adapter `<parent>:<profile>`.\n 109|    let id = \"prof-ep\";\n 110|    let sid = \"prof-ep-sid\";\n 111|    let created_adapter = \"dummyharness:fast\";\n 112|    let parent_only = \"dummyharness\"; // what the agnostic hook-bind resolves.\n*113|    seed_perch_with_adapter(id, sid, created_adapter);\n 114|\n*115|    // Precondition: the perch starts stamped with the FULL composite option.\n 116|    assert_eq!(\n 117|        recorded_adapter(id).as_deref(),\n 118|        Some(created_adapter),\n*119|        \"PRECONDITION: the perch must start stamped with the composite adapter\"\n 120|    );\n 121|\n 122|    // THE RE-BIND, via the REAL bind path: `api bind` presenting the SAME session id\n 123|    // (a reconnect, not a conflict) with an incoming BARE `<parent>` adapter — exactly\n*124|    // what the agnostic hook-bind resolves. `bind` routes establish_perch →\n 125|    // stamp_creation_fields; the profile-preserving precedence must KEEP the composite.\n 126|    let out = {\n 127|        let mut cmd = Command::new(&spt_bin);\n## bind_cwd_project_e2e.rs\n 8|//! `dummy_harness_e2e.rs` — `spt endpoint run --start` → broker PTY → the dummy\n*9|//! harness shells `spt api bind` — and then asserts the live perch records a\n 10|//! non-empty `info.cwd`, and that the cwd-derived project membership (the exact\n 11|//! `project_id_for_dir` + origin-union rule the picker's `local_rows` runs)\n 12|//! places the endpoint under its project.\n...\n 18|//! The pure union rule is unit-tested in `picker/data.rs`\n*19|//! (`origin_project_unions_into_membership`); this test proves the REAL perch\n 20|//! carries the cwd that rule consumes.\n 21|//!\n 22|//! Bounded; all spawned pids reaped SCOPED (never machine-wide — shared runner).\n...\n 30|\n*31|use spt_store::perch::{self, ParentHint};\n 32|\n 33|fn kill_pid(pid: u32) {\n 34|    #[cfg(windows)]\n...\n 93|// [int->REQ-HAZARD-BIND-CWD-UNSET] a REAL spt-hosted endpoint-run dummy-harness\n*94|// perch records a non-empty `info.cwd` (was NEVER set pre-W3 — the refuted P1),\n 95|// and the cwd-derived project membership (the picker's data path) places the\n 96|// endpoint under its project. The fresh endpoint, with no committed context,\n 97|// appears under its origin project SOLELY because of the recorded cwd.\n...\n 119|    //    dummy_harness_e2e.rs: a long-lived live_agent fixture that binds its\n*120|    //    perch + heartbeats, declaring [session.psyche_init] so cmd_bind marks\n 121|    //    it online). ──\n 122|    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\n 123|    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\n...\n 127|    );\n*128|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*129|    std::fs::create_dir_all(&src).unwrap();\n*130|    let psyche_bin = src.join(format!(\"psychebin{exe_suffix}\"));\n 131|    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n 132|    let drops = home.path().join(\"drops\");\n 133|    std::fs::create_dir_all(&drops).unwrap();\n...\n 140|    );\n*141|    std::fs::write(src.join(\"manifest.toml\"), &manifest_toml).unwrap();\n*142|    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 143|\n 144|    // ── (3) The REAL daemon broker process. The broker inherits THIS process's\n 145|    //    cwd; the W1 endpoint-run shortcut launches the harness with cwd=None\n...\n 181|\n*182|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n*183|    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\n 184|\n*185|    // Wait for the dummy to bind its perch ONLINE (proves the bind ran — and a\n 186|    // bind is where cwd is stamped).\n 187|    let mut online = false;\n 188|    let deadline = Instant::now() + Duration::from_secs(20);\n...\n 196|\n*197|    // Read the bound perch's recorded cwd (the W3 field).\n*198|    let info = spt_store::info::read_info(&self_perch);\n 199|    let recorded_cwd = info.as_ref().and_then(|i| i.cwd.clone());\n 200|\n 201|    // The harness pid (long-lived PTY child) — reap target.\n...\n 223|    }\n*224|    let psyche_perch =\n*225|        perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\n*226|    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\n 227|        kill_pid(p);\n 228|    }\n 229|    let _ = {\n...\n 244|        online,\n*245|        \"the dummy harness must bind its perch ONLINE (the bind is where cwd is stamped)\"\n 246|    );\n 247|\n*248|    // The real-fixture gate: a freshly bound perch records a non-empty cwd.\n 249|    let cwd = recorded_cwd.expect(\n## bind_honest_cross_perch_e2e.rs\n 2|//! contamination is structurally impossible: the dead-owner re-pin rescue REFUSES\n*3|//! to rebind a perch when the incoming session id AFFIRMATIVELY owns ANOTHER perch.\n 4|//!\n*5|//! The field contamination (doyle RCA 2026-07-03): a nested psyche's SessionStart\n*6|//! hook mis-resolved its \"self\" id to a FOREIGN, DEAD top-level perch (hall-a, whose\n 7|//! harness had exited) and fired an id-scoped `api` call presenting the PSYCHE's own\n 8|//! session id. The dead-owner rescue (REQ-HAZARD-SESSION-PIN-WEDGE) then re-pinned\n 9|//! hall-a's `info.json.session_id` to the psyche's session (359d7bd7) — the observed\n*10|//! corruption. The fix (`auth.rs` `sid_owns_another_perch`): a re-pin whose incoming\n*11|//! sid has an HONEST HOME on another perch is a cross-perch mis-stamp, never a\n 12|//! same-session reconnect — so it is refused and the victim's pin is untouched.\n 13|//!\n 14|//! This drives the REAL auth path — `spt api presence`, which routes through\n*15|//! `gated()` → `authenticate()` — over the REAL `spt` binary against a REAL detached\n 16|//! `spt daemon run`. `spt` is bin-only (no lib), so the binary IS the only entry to\n*17|//! the real auth path; there is no mock. TWO perches: victim A (dead owner) and the\n 18|//! session's honest home B. An `api presence --id A --session-id <B's sid>` must be\n 19|//! REFUSED and leave A's pin untouched (no contamination stamp lands on A).\n 20|//!\n...\n 30|use spt_store::info::{self, InfoJson};\n*31|use spt_store::perch::{self, ParentHint};\n 32|\n 33|/// A DEFINITELY-DEAD pid (above Linux pid_max and Windows ranges; positive-as-i32\n 34|/// so Unix kill-group semantics don't misfire). Mirrors the auth unit's DEAD_PID —\n...\n 77|\n*78|/// Create a top-level perch pinned to `session_id`, recording owner `pid`.\n*79|fn seed_perch(id: &str, session_id: &str, pid: u32, state: &str) -> PathBuf {\n*80|    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n 81|    std::fs::create_dir_all(&path).unwrap();\n 82|    info::write_info(&path, &InfoJson::new(id, \"2026-06-01T00:00:00Z\", pid, session_id, state)).unwrap();\n 83|    path\n...\n 90|#[test]\n*91|fn cross_perch_dead_owner_repin_is_refused_no_contamination() {\n 92|    // ── (1) Isolated, NET-LESS SPT_HOME (auth is pure filesystem; no net needed). ──\n 93|    let home = tempfile::tempdir().unwrap();\n 94|    std::env::set_var(\"SPT_HOME\", home.path());\n...\n 100|\n*101|    // ── (2) TWO perches. ──\n*102|    // Victim A: a foreign top-level LIVE_AGENT perch whose owner pid is DEAD (its\n*103|    // harness exited) — the perch the dead-owner rescue would otherwise re-pin.\n 104|    let victim_id = \"hall-a\";\n 105|    let victim_sid = \"hall-a-own-sid\";\n*106|    let victim = seed_perch(victim_id, victim_sid, DEAD_PID, \"live_agent\");\n*107|    // The session's HONEST HOME B: a perch the incoming sid AFFIRMATIVELY owns (the\n 108|    // nested psyche that legitimately holds 359d7bd7 in the field). Its liveness is\n 109|    // irrelevant to the guard — what matters is that the sid has a home elsewhere.\n 110|    let foreign_sid = \"359d7bd7\";\n*111|    let owner_b = seed_perch(\"sess-owner-b\", foreign_sid, std::process::id(), \"psyche\");\n 112|\n 113|    // ── (3) The REAL daemon broker process (doyle: real daemon; also keeps\n 114|    //    `ensure_daemon` on the api call a no-op so no stray daemon leaks). ──\n...\n 146|        foreign_sid,\n*147|        \"PRECONDITION: the honest-home perch must own the incoming sid\"\n 148|    );\n 149|\n 150|    // ── (4) THE ATTACK, via the REAL auth path: an id-scoped `api presence --id A`\n...\n 152|    //    psyche hook fired. `presence` routes gated() → authenticate(); the dead-owner\n*153|    //    rescue must REFUSE because the sid owns another perch (bind honesty). ──\n 154|    let attack = {\n 155|        let mut cmd = Command::new(&spt_bin);\n 156|        cmd.no_window()\n## bootstrap_verb_e2e.rs\n*1|//! THE-FORKENING W1 — the self-install verb, driven end to end as a virgin\n 2|//! box would use it (ADR-0036 §3, bootstrap path b): the release binary is\n*3|//! \"downloaded\" under its `spt-*` asset name (never an installer-detection\n*4|//! trigger word), runs `install` against a clean simulated `SPT_HOME`, lands\n 5|//! at the canonical `<SPT_HOME>/bin` path, is idempotent on re-run, and the\n*6|//! PLACED binary's `spt update fetch` speaks the gh channel — proven by the\n 7|//! loud `GhCliRequired` refusal when no `gh` is resolvable, which is the gh\n 8|//! carrier gate itself, not an HTTPS fallback.\n 9|//!\n...\n 12|//!\n*13|//! Named `bootstrap_verb_e2e`, NOT `install_verb_e2e`: an exe whose NAME\n*14|//! carries install/setup/update trips Windows installer-detection (UAC os\n 15|//! error 740) — the test binary itself proved the gotcha the verb's `spt-*`\n 16|//! asset-name rule exists to dodge.\n 17|//\n*18|// [int->REQ-INSTALL-BOOTSTRAP-VERB]\n 19|\n 20|use std::process::Command;\n 21|\n...\n 30|\n*31|fn installed_name() -> &'static str {\n 32|    if cfg!(windows) {\n 33|        \"spt.exe\"\n 34|    } else {\n...\n 39|#[test]\n*40|fn downloaded_binary_self_installs_and_speaks_gh_channel() {\n 41|    let tmp = tempfile::tempdir().expect(\"tempdir\");\n 42|    let home = tmp.path().join(\"spt-home\");\n 43|    std::fs::create_dir_all(&home).unwrap();\n...\n 48|\n*49|    // 1. Self-install into the clean home (default dir = <SPT_HOME>/bin).\n 50|    let out = Command::new(&downloaded)\n*51|        .args([\"install\", \"--no-path\"])\n 52|        .env(\"SPT_HOME\", &home)\n 53|        .output()\n*54|        .expect(\"run install\");\n 55|    let stdout = String::from_utf8_lossy(&out.stdout);\n 56|    assert!(\n 57|        out.status.success(),\n*58|        \"install failed: {stdout} {}\",\n 59|        String::from_utf8_lossy(&out.stderr)\n 60|    );\n*61|    let target = home.join(\"bin\").join(installed_name());\n 62|    assert!(target.is_file(), \"binary placed at the canonical path\");\n*63|    assert!(stdout.contains(\"INSTALL_OK\"), \"{stdout}\");\n 64|\n 65|    // 2. Idempotent re-run — still green, same target.\n 66|    let out2 = Command::new(&downloaded)\n*67|        .args([\"install\", \"--no-path\"])\n 68|        .env(\"SPT_HOME\", &home)\n 69|        .output()\n*70|        .expect(\"re-run install\");\n 71|    assert!(\n 72|        out2.status.success(),\n 73|        \"re-run must stay green: {}\",\n...\n 77|    // 3. The PLACED binary works and its update fetch speaks the gh channel:\n*78|    //    with no resolvable gh the carrier gate refuses LOUD with the install\n 79|    //    hint — the gh mandate itself, before any network or key work.\n 80|    let empty_path = tmp.path().join(\"empty-path-dir\");\n 81|    std::fs::create_dir_all(&empty_path).unwrap();\n...\n 86|        .output()\n*87|        .expect(\"run placed spt update fetch\");\n 88|    let stderr = String::from_utf8_lossy(&fetch.stderr);\n 89|    assert!(!fetch.status.success(), \"fetch without gh must refuse\");\n 90|    assert!(\n## boundary_ready_strand_e2e.rs\n 1|//! F-029 C-2 (REQ-HAZARD-BOUNDARY-READY-STRAND) — SEAM-2, B6's second half. A\n*2|//! `/clear` boundary must LEAVE the perch a live inject target: the pre-boundary\n 3|//! soft `api session-end` (which CC fires for the DEPARTING session before\n 4|//! SessionStart, while its sid still matches the pin) removes the ready marker; the\n*5|//! boundary must re-stamp it, or every post-clear force-native (incl. the checkpoint\n 6|//! FIRE) reports the generic UNDELIVERED forever (is_online false → the CLI inject\n 7|//! gate Nones before any broker RPC).\n 8|//!\n 9|//! This drives the REAL boundary path over the REAL `spt` binary: a bound,\n*10|//! controllable, online perch → real soft `api session-end` (ready gone,\n 11|//! is_spt_hosted_no_relay false) → real `api boundary clear` → ready PRESENT +\n 12|//! is_spt_hosted_no_relay TRUE again. RED on 0.23.0 (the boundary leaves ready\n 13|//! absent). Any auto-started daemon against the throwaway home is reaped scoped.\n...\n 23|use spt_store::liveness::STATUS_ONLINE;\n*24|use spt_store::perch::{self, ParentHint};\n 25|\n 26|fn start_inproc_daemon() {\n*27|    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n 28|    let name = spt_daemon::seed_socket_name();\n 29|    std::thread::spawn(move || {\n 30|        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);\n...\n 50|\n*51|/// Seed a fully BOUND spt-hosted perch: online + controllable + ready marker +\n 52|/// live owner pid (this process) — an `is_spt_hosted_no_relay` inject target.\n*53|fn seed_bound_perch(id: &str, session_id: &str) {\n*54|    let path = perch::resolve_perch_path(id, ParentHint::Infer);\n 55|    std::fs::create_dir_all(&path).unwrap();\n 56|    let mut rec = InfoJson::new(\n 57|        id,\n...\n 65|    info::write_info(&path, &rec).unwrap();\n*66|    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 67|}\n 68|\n 69|#[test]\n...\n 81|    std::fs::write(&manifest_path, manifest_toml).unwrap();\n*82|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n*83|    std::fs::create_dir_all(&src).unwrap();\n*84|    std::fs::write(src.join(\"manifest.toml\"), manifest_toml).unwrap();\n*85|    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n 86|    let mp = manifest_path.to_string_lossy().to_string();\n 87|\n 88|    let id = \"clr-ep\";\n...\n 90|    let new_sid = \"clr-ep-sid-2\";\n*91|    seed_bound_perch(id, sid);\n 92|\n*93|    let owlery = perch::owlery_dir();\n*94|    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n 95|    let spt = |args: &[&str]| {\n 96|        let mut cmd = Command::new(&spt_bin);\n 97|        cmd.no_window()\n...\n 103|\n*104|    // Precondition: a bound perch is a live inject target.\n 105|    assert!(\n 106|        spt_daemon::is_spt_hosted_no_relay(id, &owlery),\n*107|        \"PRECONDITION: a bound online+controllable+ready perch is an inject target\"\n 108|    );\n 109|\n 110|    // ── The pre-boundary soft session-end (CC fires it for the departing session,\n...\n 117|        !spt_daemon::is_spt_hosted_no_relay(id, &owlery),\n*118|        \"with ready gone the perch is NO LONGER an inject target (the SEAM-2 strand)\"\n 119|    );\n 120|\n 121|    // ── THE BOUNDARY: rotates the sid AND (C-2) re-stamps ready. REAL `api boundary\n...\n 130|    let target_after = spt_daemon::is_spt_hosted_no_relay(id, &owlery);\n*131|    let sid_after = info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))\n 132|        .map(|r| r.session_id);\n 133|    eprintln!(\n 134|        \"=== F029 C-2 DIAGNOSTIC: boundary_exit={:?} ready_after={ready_after} \\\n## brain_respawn_rename.rs\n 7|//! distinct fixtures through an injected spawn closure — it never renames a\n*8|//! *running* binary, so it cannot exercise the production respawn resolution\n 9|//! (`std::env::current_exe()` in the resident broker). That gap let the v0.4.1\n 10|//! Linux brain-respawn bug ship green: on Linux `current_exe()` is\n 11|//! `readlink(/proc/self/exe)`, **inode-tracking**, so it FOLLOWS the `apply`\n...\n 64|\n*65|/// Lowercase-hex SHA-256 of a file — the digest `brain.ready.exe_hash` carries.\n 66|fn file_hash(path: &Path) -> String {\n 67|    use sha2::{Digest, Sha256};\n 68|    let bytes = std::fs::read(path).expect(\"read file for hashing\");\n...\n 94|\n*95|    // The canonical install path P the broker runs from — so the broker's own\n 96|    // current_exe IS P (the resident-broker condition the bug needs).\n 97|    let canonical = bindir.join(format!(\"spt{suffix}\"));\n*98|    std::fs::copy(&real, &canonical).expect(\"install bytes A at canonical P\");\n 99|    let hash_a = file_hash(&canonical);\n 100|\n 101|    // The REAL broker process, launched FROM P.\n...\n 117|        Some(hash_a.as_str()),\n*118|        \"gen-0 brain runs bytes A (the canonical install)\"\n 119|    );\n 120|\n 121|    // ── The real apply choreography on disk: rename the running canonical binary\n 122|    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\n*123|    //    what `spt update apply` does (rename-then-replace). ──\n 124|    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\n 125|    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\n 126|    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\n## brain_survive.rs\n 20|//!   in-process [`Broker`] runs the **same Broker code holding the same\n*21|//!   resources** the real broker would across a brain swap. The claim under test\n 22|//!   is the brain process + the swapped binary + broker-held endpoint survival —\n 23|//!   all real here. (Bonus: the in-process broker gives the test direct access to\n 24|//!   its session pid / count and QUIC conn count for the assertions.)\n...\n 40|//! `exe_hash` breadcrumb (`exe_hash == fixture-B != fixture-A`). It is also the\n*41|//! **first process-level exercise of the D4 cursor-of-record resume with N≥1\n 42|//! hosted sessions** (the supervised child re-attaches the test-established PTY\n 43|//! session on cold start).\n 44|//!\n...\n 63|use std::sync::atomic::{AtomicBool, Ordering};\n*64|use std::sync::{Arc, Mutex};\n 65|use std::thread;\n 66|use std::time::{Duration, Instant};\n 67|\n...\n 99|        adapter: String::new(),\n*100|        install_dir: None,\n 101|    }\n 102|}\n 103|\n...\n 117|/// endpoint for `identity`, with its effect journal under `dir`.\n*118|fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\n 119|    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\n 120|    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n 121|        .expect(\"bind broker with net\");\n*122|    let serve = Arc::clone(&broker);\n 123|    thread::spawn(move || {\n 124|        let _ = serve.serve();\n 125|    });\n...\n 172|\n*173|/// Lowercase-hex SHA-256 of a file (the same digest `brain.ready.exe_hash`\n 174|/// carries; recomputed independently here to assert the breadcrumb matches the\n 175|/// fixture the supervisor actually spawned).\n 176|fn file_hash(path: &std::path::Path) -> String {\n...\n 248|        (sid, child_pid)\n*249|        // driver dropped here — HARD detach (no graceful handoff frame).\n 250|    };\n 251|\n 252|    // ── Start the production supervisor, spawning the REAL `spt daemon brain`\n...\n 256|    let ready_path = home.path().join(\"brain.ready\");\n*257|    let stop = Arc::new(AtomicBool::new(false));\n*258|    let restart = Arc::new(BrainRestart::new());\n*259|    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260|    let home_path = home.path().to_path_buf();\n 261|\n 262|    let sup = {\n*263|        let stop = Arc::clone(&stop);\n*264|        let restart = Arc::clone(&restart);\n*265|        let selected = Arc::clone(&selected);\n 266|        thread::spawn(move || {\n 267|            supervise_brain(\n 268|                &stop,\n## composite_e2e.rs\n*1|//! THE-FORKENING W4 — the bare `spt update` composite, end to end\n 2|//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\n 3|//! release, THEN updates a registered gh_release adapter — core-first order,\n 4|//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\n...\n 13|//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\n*14|//! Windows' installer-detection heuristic forces UAC elevation on a\n 15|//! manifest-less exe whose name contains it (os error 740; the\n 16|//! post_step_fixture precedent).\n 17|//\n...\n 51|    rx.recv_timeout(deadline)\n*52|        .expect(\"spt update must complete within its deadline\")\n*53|        .expect(\"run spt update\")\n 54|}\n 55|\n 56|/// The signed update set the fake channel serves: one artifact for the CURRENT\n...\n 68|                brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n*69|                broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,\n 70|                asset_name: Some(\"spt-composite-artifact\".to_string()),\n 71|            },\n 72|        )]),\n...\n 85|\n*86|/// Build a `.spt` adapter release archive (tar, relative members — the exact\n*87|/// shape `extract_release_archive` consumes) whose root manifest declares\n 88|/// `version` and keeps the gh_release `[update]` block (so the re-register\n 89|/// after the swap preserves the avenue).\n 90|fn adapter_spt(scratch: &Path, version: &str) -> Vec<u8> {\n...\n 165|\n*166|    // ── (3) Register the gh_release adapter `cc` at 1.0.0 (its install dir is\n*167|    //    the registered source_dir — the in-place update target). ──\n 168|    std::env::set_var(\"SPT_HOME\", &home);\n*169|    let install_dir = spt_store::perch::spt_home().join(\"srcs\").join(\"cc\");\n*170|    std::fs::create_dir_all(&install_dir).unwrap();\n*171|    std::fs::write(install_dir.join(\"manifest.toml\"), adapter_manifest(\"1.0.0\")).unwrap();\n*172|    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &install_dir, 1000)\n 173|        .unwrap();\n 174|    std::env::remove_var(\"SPT_HOME\");\n 175|\n*176|    // ── (4) ONE invocation: bare `spt update`. ──\n 177|    let out = {","truncated":true,"truncatedBy":"bytes","totalLines":1108,"totalBytes":68333,"outputLines":743,"outputBytes":51198,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":1108,"totalBytes":68333,"outputLines":743,"outputBytes":51198,"shownRange":{"start":1,"end":743},"nextOffset":744}}},"isError":false,"timestamp":1784195765988}}
{"type":"message","id":"52485f02","parentId":"92b5a7e9","timestamp":"2026-07-16T09:56:18.068Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating output filename mismatch**"},{"type":"toolCall","id":"call_pYllDwrgPWRaFRZJCgU5sx3x|fc_0b42584a8ab0545d016a58aac206ec8199927b500aab6ac7fe","name":"glob","arguments":{"i":"Locating update incident docs","path":"docs/**/*WEDGE*;docs/**/*wedge*;docs/**/*UPDATE*ROOT*;docs/**/*RCA*;docs/**/*update*","hidden":true,"gitignore":true,"limit":300}},{"type":"toolCall","id":"call_tAlJxHySkoZva8SidnnP5qvz|fc_0b42584a8ab0545d016a58aac206fc81999911dd0f88aa389d","name":"grep","arguments":{"i":"Finding incident citations","pattern":"UPDATE-WEDGE|22:47|v0\\.27|v0\\.28|refresh.*detach|detach.*refresh|reattach.*refresh|most.*session|all.*session.*except|until.*rc","path":"docs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_jBbdZAavYPrns1VSyLkZwCEp|fc_0b42584a8ab0545d016a58aac207048199a36698bbcce7bfde","name":"read","arguments":{"i":"Reading real daemon attach harness","path":"crates/spt/tests/attach_wedge_e2e.rs","selector":"1-360"}},{"type":"toolCall","id":"call_xwVmvMU59AbnZzMwkTnkKaCe|fc_0b42584a8ab0545d016a58aac207088199b07f1645c7e43b80","name":"read","arguments":{"i":"Reading update composite harness","path":"crates/spt/tests/composite_e2e.rs","selector":"1-300"}},{"type":"toolCall","id":"call_dAgqmkZhli4yXkIDCsLWANT9|fc_0b42584a8ab0545d016a58aac207108199ba39943628452d03","name":"read","arguments":{"i":"Reading brain restart hazard harness","path":"crates/spt-daemon/tests/brain_decouple.rs","selector":"1-430"}},{"type":"toolCall","id":"call_F3wgFaEbj2c69XwqXwDW2wGX|fc_0b42584a8ab0545d016a58aac2071881998a1abec6ffd2ddda","name":"read","arguments":{"i":"Reading resume deadlock harness","path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","selector":"1-390"}},{"type":"toolCall","id":"call_8I27EUjL4h1nOgB68S8YRHxe|fc_0b42584a8ab0545d016a58aac2071c8199862b51d6b6ba5663","name":"grep","arguments":{"i":"Finding executable test commands","pattern":"cargo (test|nextest)|daemon_refresh_e2e|brain_survive|attach_survives_target|controller_restart_with_viewer|brain_decouple|brain_resume_conn_deadlock|attach_wedge_e2e","path":".github;docs;Makefile*;justfile;Cargo.toml;crates","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":35981,"output":458,"cacheRead":74240,"cacheWrite":0,"totalTokens":110679,"reasoningTokens":34,"cost":{"input":0.179905,"output":0.01374,"cacheRead":0.03712,"cacheWrite":0,"total":0.230765}},"stopReason":"toolUse","timestamp":1784195766032,"responseId":"resp_0b42584a8ab0545d016a58aab67ce48199a9e0a9b4dd023409","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aab80de081998c8bd2d4fac93feb","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrCfAeiiP_zry3DG3VIeY8nSCERZaBeiiELUvF0Ki8XYXxeWACvi2zld5ckpMwY8j_5BXLqWOv-lOBm7x-VhAbQa9r9q9xVT4jVLWtJJH4A8hd3VAvmU19OW-GLB8ifE0MY2a7nTBRUjpDzLa4cgXEovAch3-uk9-rIjNLYQ_RKfjXvc4VdFAMnBdqkD4wjnBxqGbPXf5idPE_mgXp2e_jBm9DAPIAEqPFUNgs3OOlvyeFsfDry4MyqoD5lqzF56QtYXyp3WDDlTaOBaJhSTW2hJkU97WOqqi8q-t2svt-Gzgv1BjaWAbKZbqfwIE7HM_c5eRck3O7xeGJnhHut39FzOzIydQKQmfcTflxxTKMQ-QGP_H2h2_3hgWPr3lz5nBwxUeCpLqhqlaSs-4EmXXLGM4oA8tYKxSw9qA1ZvLRnNWHth0PWPum0oQsJ4onj1dqPLLrW-dchu3Zo77ke-nRqT5kUxh-aeLP2I6954l9LDT9zjr6bRX37IcpUVZjdUT83n9zhBFfCCecVbRsUcfGGJd0Hkhj7psmJCb3zNPar39VugadKIpFJSkgOe6KL4k4n8ojASQfaprPbUJtu4bcVI4mV391d4Nr23HXhNzi_m-s15cCMqmWAEQ8eBS94oYdiQRIsKrlM2zq7CcZ3d8t1caeRQwPP4ueVhU8VUlGNeXHd9xAB7_fPcarWcRvdBt3ApjIqTQcmJVrC0sRCsbL8-Rez6dl84QBCVACmsbZeJha219GT3RnqELT89T709vxeX1x5eDwmuQ4zrvR-zwiMHsHZyIgOZa0RH2Y8gj-bKBngv_OwXSPotqrjqRXgUBtaacBxdNDsg5r5ByiKjO-R0FSwem03RterC6tSucgN52FTzmhKZS6JT4sU9qY6cpsU_4qnLHq4SYb9hX5rC01x0sElQEa_BEkE2-F2NA9t-axCODElyR4doWwGDAQOVuT9GzDhl4F4a4Wl-ISqfgPnsurx24NgtybxYfWvypmTXLi62YpbJUWn5QUz6doem9SdVkWHBjuPicUyYCWhW0U4sELPy8Zy-rvUWvEqxW9J958-wF5hcJPPqkjmKqO5fP_ySg1JExNPG3RO_7W5-zey4fEJ4Dbz30T-o9ITtKONFo0U6Pl7toNOYupbxDxC_yR7","summary":[{"type":"summary_text","text":"**Investigating output filename mismatch**"}]},{"id":"fc_0b42584a8ab0545d016a58aac206ec8199927b500aab6ac7fe","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating update incident docs\",\"path\":\"docs/**/*WEDGE*;docs/**/*wedge*;docs/**/*UPDATE*ROOT*;docs/**/*RCA*;docs/**/*update*\",\"hidden\":true,\"gitignore\":true,\"limit\":300}","call_id":"call_pYllDwrgPWRaFRZJCgU5sx3x","name":"glob"},{"id":"fc_0b42584a8ab0545d016a58aac206fc81999911dd0f88aa389d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding incident citations\",\"pattern\":\"UPDATE-WEDGE|22:47|v0\\\\.27|v0\\\\.28|refresh.*detach|detach.*refresh|reattach.*refresh|most.*session|all.*session.*except|until.*rc\",\"path\":\"docs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_tAlJxHySkoZva8SidnnP5qvz","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aac207048199a36698bbcce7bfde","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading real daemon attach harness\",\"path\":\"crates/spt/tests/attach_wedge_e2e.rs\",\"selector\":\"1-360\"}","call_id":"call_jBbdZAavYPrns1VSyLkZwCEp","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aac207088199b07f1645c7e43b80","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update composite harness\",\"path\":\"crates/spt/tests/composite_e2e.rs\",\"selector\":\"1-300\"}","call_id":"call_xwVmvMU59AbnZzMwkTnkKaCe","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aac207108199ba39943628452d03","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain restart hazard harness\",\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"selector\":\"1-430\"}","call_id":"call_dAgqmkZhli4yXkIDCsLWANT9","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aac2071881998a1abec6ffd2ddda","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading resume deadlock harness\",\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"selector\":\"1-390\"}","call_id":"call_F3wgFaEbj2c69XwqXwDW2wGX","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aac2071c8199862b51d6b6ba5663","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding executable test commands\",\"pattern\":\"cargo (test|nextest)|daemon_refresh_e2e|brain_survive|attach_survives_target|controller_restart_with_viewer|brain_decouple|brain_resume_conn_deadlock|attach_wedge_e2e\",\"path\":\".github;docs;Makefile*;justfile;Cargo.toml;crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_8I27EUjL4h1nOgB68S8YRHxe","name":"grep"}]},"duration":12034.475200000219,"ttft":1999.9443000005558,"contextSnapshot":{"promptTokens":110221,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pYllDwrgPWRaFRZJCgU5sx3x|fc_0b42584a8ab0545d016a58aac206ec8199927b500aab6ac7fe","toolName":"glob","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":"docs/**/*WEDGE*;docs/**/*wedge*;docs/**/*UPDATE*ROOT*;docs/**/*RCA*;docs/**/*update*"},"intent":"Locating update incident docs"},"id":"26c56d72","parentId":"52485f02","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_tAlJxHySkoZva8SidnnP5qvz|fc_0b42584a8ab0545d016a58aac206fc81999911dd0f88aa389d","toolName":"grep","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":"docs"},"intent":"Finding incident citations"},"id":"e3b45912","parentId":"26c56d72","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jBbdZAavYPrns1VSyLkZwCEp|fc_0b42584a8ab0545d016a58aac207048199a36698bbcce7bfde","toolName":"read","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":"crates/spt/tests/attach_wedge_e2e.rs"},"intent":"Reading real daemon attach harness"},"id":"9dffa6ec","parentId":"e3b45912","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xwVmvMU59AbnZzMwkTnkKaCe|fc_0b42584a8ab0545d016a58aac207088199b07f1645c7e43b80","toolName":"read","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":"crates/spt/tests/composite_e2e.rs"},"intent":"Reading update composite harness"},"id":"5461c2db","parentId":"9dffa6ec","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dAgqmkZhli4yXkIDCsLWANT9|fc_0b42584a8ab0545d016a58aac207108199ba39943628452d03","toolName":"read","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":"crates/spt-daemon/tests/brain_decouple.rs"},"intent":"Reading brain restart hazard harness"},"id":"badd2337","parentId":"5461c2db","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_F3wgFaEbj2c69XwqXwDW2wGX|fc_0b42584a8ab0545d016a58aac2071881998a1abec6ffd2ddda","toolName":"read","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs"},"intent":"Reading resume deadlock harness"},"id":"1f80738b","parentId":"badd2337","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8I27EUjL4h1nOgB68S8YRHxe|fc_0b42584a8ab0545d016a58aac2071c8199862b51d6b6ba5663","toolName":"grep","startedAt":"2026-07-16T09:56:18.069Z","args":{"path":".github;docs;Makefile*;justfile;Cargo.toml;crates"},"intent":"Finding executable test commands"},"id":"283a4ee5","parentId":"1f80738b","timestamp":"2026-07-16T09:56:18.069Z"}
{"type":"message","id":"1a2cd0f4","parentId":"283a4ee5","timestamp":"2026-07-16T09:56:18.075Z","message":{"role":"toolResult","toolCallId":"call_pYllDwrgPWRaFRZJCgU5sx3x|fc_0b42584a8ab0545d016a58aac206ec8199927b500aab6ac7fe","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\n## adr/\n0025-live-daemon-coordinated-adapter-update.md\n0029-adapter-update-arc-resolve-not-execute.md\n0016-platform-targeted-update-sets.md\n0004-single-daemon-broker-brain-split-and-self-update.md\n## design/\nv0.16.0-update-arc-and-cli.md"}],"details":{"scopePath":"docs/**/*WEDGE*, docs/**/*wedge*, docs/**/*UPDATE*ROOT*, docs/**/*RCA*, docs/**/*update*","fileCount":10,"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/adr/0025-live-daemon-coordinated-adapter-update.md","docs/design/v0.16.0-update-arc-and-cli.md","docs/adr/0029-adapter-update-arc-resolve-not-execute.md","docs/adr/0016-platform-targeted-update-sets.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784195778075}}
{"type":"message","id":"165482b1","parentId":"1a2cd0f4","timestamp":"2026-07-16T09:56:18.076Z","message":{"role":"toolResult","toolCallId":"call_jBbdZAavYPrns1VSyLkZwCEp|fc_0b42584a8ab0545d016a58aac207048199a36698bbcce7bfde","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":1784195778076}}
{"type":"message","id":"214a3092","parentId":"165482b1","timestamp":"2026-07-16T09:56:18.078Z","message":{"role":"toolResult","toolCallId":"call_F3wgFaEbj2c69XwqXwDW2wGX|fc_0b42584a8ab0545d016a58aac2071881998a1abec6ffd2ddda","toolName":"read","content":[{"type":"text","text":"//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n//!\n//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n//! onto the brain's OWN request/reply conn. A conn's send half is one\n//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n//! calls. When an actively-streaming session backs the conn up, the brain's own\n//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n//! wedge (both severities: respawn replay-burst + steady-state output backup), with\n//! NO dead peer required.\n//!\n//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\n//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n//! a truly deadlocked one completes ~zero).\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::brain::Brain;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\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-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\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\nfn kill_pid(pid: u32) {\n    if pid == 0 {\n        return; // unknown pid — never signal pid 0 (unix process group)\n    }\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\n/// A child that FLOODS stdout forever — the heavy-output case that fills a\n/// non-draining subscriber's conn and parks its writer inside `write_frame`.\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\n            \"/c\".to_string(),\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\n        ],\n    );\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\nfn connect(name: &str) -> Stream {\n    for _ in 0..200 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Spawn one flood PTY session; return (session_id, pid, spawner_conn). The conn is\n/// kept alive (the spawner is the local by:None controller) but NOT drained — the\n/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n/// brain's viewer subscription keeps receiving live output.\nfn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n    let mut c = connect(name);\n    write_frame(\n        &mut c,\n        &Envelope::new(KIND_SPAWN, serde_json::to_value(flood_spawn_req(endpoint)).unwrap()),\n    )\n    .expect(\"send spawn\");\n    loop {\n        let f = read_frame(&mut c).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            let s: Spawned = serde_json::from_value(f.payload).unwrap();\n            return (s.session_id, s.pid.unwrap_or(0), c);\n        }\n    }\n}\n\n/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n#[derive(Clone, Copy)]\nenum ResumeMode {\n    CursorOnly,\n}\n\n/// Drive `run_brain`'s heartbeat pattern on a fresh `Brain` conn: resume per `mode`,\n/// then loop `net_status()` + `sessions()` — the exact per-tick IPC — incrementing a\n/// shared counter per COMPLETED tick, so the caller can measure liveness in a window.\n/// The probe NEVER drains subscribed output (it only issues request/reply, exactly like\n/// the real non-draining daemon heartbeat) — so `CursorOnly` faithfully models the\n/// shipping daemon heartbeat under flood.\nfn run_heartbeat_probe(\n    name: &str,\n    mode: ResumeMode,\n    ticks: Arc<AtomicU32>,\n    stop: Arc<std::sync::atomic::AtomicBool>,\n) {\n    let name = name.to_string();\n    thread::spawn(move || {\n        let mut brain = match Brain::cold_start(&name, 0) {\n            Ok(b) => b,\n            Err(_) => return,\n        };\n        match mode {\n            // Shipping daemon path: seed cursors, NO subscribe.\n            ResumeMode::CursorOnly => {\n                let _ = brain.resume_session_cursors();\n            }\n        }\n        while !stop.load(Ordering::Relaxed) {\n            // One heartbeat tick = the two IPC round-trips run_brain issues.\n            if brain.net_status().is_err() {\n                return;\n            }\n            if brain.sessions().is_err() {\n                return;\n            }\n            ticks.fetch_add(1, Ordering::Relaxed);\n        }\n    });\n}\n\n/// How many heartbeat ticks complete in `window` for a probe configured with `mode`.\n/// N flood PTY sessions are already streaming before the probe starts.\nfn heartbeat_ticks_under_flood(mode: ResumeMode, n_sessions: usize, window: Duration) -> u32 {\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 flooding PTY sessions the brain will resume.\n    let mut conns = Vec::new();\n    let mut pids = Vec::new();\n    for i in 0..n_sessions {\n        let (_sid, pid, conn) = spawn_flood(&name, &format!(\"flood-ep-{i}\"));\n        pids.push(pid);\n        conns.push(conn); // hold the spawner controller conns open (undrained)\n    }\n    // Let the floods get going so the brain conn genuinely backs up on subscribe.\n    thread::sleep(Duration::from_millis(600));\n\n    let ticks = Arc::new(AtomicU32::new(0));\n    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n    run_heartbeat_probe(&name, mode, Arc::clone(&ticks), Arc::clone(&stop));\n\n    thread::sleep(window);\n    stop.store(true, Ordering::Relaxed);\n    let count = ticks.load(Ordering::Relaxed);\n\n    for pid in pids {\n        kill_pid(pid);\n    }\n    drop(conns);\n    count\n}\n\n/// STEADY-STATE severity — the SHIPPING path: the daemon brain resumes CURSOR-ONLY\n/// (`resume_session_cursors`, no subscribe) while N sessions actively stream, so its\n/// heartbeat stays live. NO dead peer anywhere. RED-first: revert run_brain to the\n/// subscribing `resume_sessions` (or see the regression-guard test below) and this\n/// starves to a standstill.\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n#[test]\nfn daemon_cursor_only_resume_keeps_heartbeat_live_steady_state() {\n    const N: usize = 6;\n    let window = Duration::from_secs(6);\n    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\n    // Healthy heartbeat is ~ms per tick → hundreds per window; the RED path lands ~0.\n    assert!(\n        ticks >= 100,\n        \"the daemon's cursor-only resume must keep the heartbeat live under a {N}-session \\\n         flood (got {ticks} ticks in {window:?}). A low count means the SharedSend \\\n         self-deadlock re-appeared — the brain is subscribing a conn it does not drain.\"\n    );\n}\n\n/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\n/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\n/// rate (the sessions predate this generation). The new generation's heartbeat — hence\n/// its readiness/promotion — is never starved.\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n#[test]\nfn daemon_cursor_only_resume_keeps_heartbeat_live_respawn_interleave() {\n    const N: usize = 6;\n    let window = Duration::from_secs(6);\n    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\n    assert!(\n        ticks >= 100,\n        \"a respawning brain's cursor-only resume must keep its heartbeat live while N={N} \\\n         sessions flood (got {ticks} ticks in {window:?}) — else the reply-starvation \\\n         deadlock stalls the new generation's readiness/promotion.\"\n    );\n}\n\n/// STRUCTURAL zero-subscriber resume guard — REPLACES the retired best-of-K flood-\n/// starvation guard (`nondraining_subscriber_starves_under_flood_regression_guard`),\n/// which went stale after the r4 Option-C fix bounded every physical-conn write (the\n/// `BrokerConn` watchdog retires a blocked write at `brain_write_deadline()`), so a\n/// non-draining subscriber no longer starves the heartbeat to ~0 and the timing-based\n/// 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    drop(hazard_brain);\n}\n"}],"details":{"displayContent":{"text":"//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\r\n//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\r\n//!\r\n//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\r\n//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\r\n//! onto the brain's OWN request/reply conn. A conn's send half is one\r\n//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\r\n//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\r\n//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\r\n//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\r\n//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\r\n//! calls. When an actively-streaming session backs the conn up, the brain's own\r\n//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\r\n//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\r\n//! wedge (both severities: respawn replay-burst + steady-state output backup), with\r\n//! NO dead peer required.\r\n//!\r\n//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\r\n//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\r\n//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\r\n//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\r\n//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\r\n//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\r\n//! a truly deadlocked one completes ~zero).\r\n//!\r\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\r\n//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\r\n//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\r\n//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\r\n//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\r\n//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\r\n\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 interprocess::local_socket::Stream;\r\nuse spt_daemon::brain::Brain;\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\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-resumedeadlock-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\r\nfn init_home() {\r\n    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    if pid == 0 {\r\n        return; // unknown pid — never signal pid 0 (unix process group)\r\n    }\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\n/// A child that FLOODS stdout forever — the heavy-output case that fills a\r\n/// non-draining subscriber's conn and parks its writer inside `write_frame`.\r\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\r\n            \"/c\".to_string(),\r\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\r\n        ],\r\n    );\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\nfn connect(name: &str) -> Stream {\r\n    for _ in 0..200 {\r\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\r\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\r\n            return c;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"could not connect to broker {name}\");\r\n}\r\n\r\n/// Spawn one flood PTY session; return (session_id, pid, spawner_conn). The conn is\r\n/// kept alive (the spawner is the local by:None controller) but NOT drained — the\r\n/// child floods the PTY regardless, so the broker's ring keeps advancing and the\r\n/// brain's viewer subscription keeps receiving live output.\r\nfn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\r\n    let mut c = connect(name);\r\n    write_frame(\r\n        &mut c,\r\n        &Envelope::new(KIND_SPAWN, serde_json::to_value(flood_spawn_req(endpoint)).unwrap()),\r\n    )\r\n    .expect(\"send spawn\");\r\n    loop {\r\n        let f = read_frame(&mut c).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            let s: Spawned = serde_json::from_value(f.payload).unwrap();\r\n            return (s.session_id, s.pid.unwrap_or(0), c);\r\n        }\r\n    }\r\n}\r\n\r\n/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\r\n/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\r\n/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\r\n/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\r\n#[derive(Clone, Copy)]\r\nenum ResumeMode {\r\n    CursorOnly,\r\n}\r\n\r\n/// Drive `run_brain`'s heartbeat pattern on a fresh `Brain` conn: resume per `mode`,\r\n/// then loop `net_status()` + `sessions()` — the exact per-tick IPC — incrementing a\r\n/// shared counter per COMPLETED tick, so the caller can measure liveness in a window.\r\n/// The probe NEVER drains subscribed output (it only issues request/reply, exactly like\r\n/// the real non-draining daemon heartbeat) — so `CursorOnly` faithfully models the\r\n/// shipping daemon heartbeat under flood.\r\nfn run_heartbeat_probe(\r\n    name: &str,\r\n    mode: ResumeMode,\r\n    ticks: Arc<AtomicU32>,\r\n    stop: Arc<std::sync::atomic::AtomicBool>,\r\n) {\r\n    let name = name.to_string();\r\n    thread::spawn(move || {\r\n        let mut brain = match Brain::cold_start(&name, 0) {\r\n            Ok(b) => b,\r\n            Err(_) => return,\r\n        };\r\n        match mode {\r\n            // Shipping daemon path: seed cursors, NO subscribe.\r\n            ResumeMode::CursorOnly => {\r\n                let _ = brain.resume_session_cursors();\r\n            }\r\n        }\r\n        while !stop.load(Ordering::Relaxed) {\r\n            // One heartbeat tick = the two IPC round-trips run_brain issues.\r\n            if brain.net_status().is_err() {\r\n                return;\r\n            }\r\n            if brain.sessions().is_err() {\r\n                return;\r\n            }\r\n            ticks.fetch_add(1, Ordering::Relaxed);\r\n        }\r\n    });\r\n}\r\n\r\n/// How many heartbeat ticks complete in `window` for a probe configured with `mode`.\r\n/// N flood PTY sessions are already streaming before the probe starts.\r\nfn heartbeat_ticks_under_flood(mode: ResumeMode, n_sessions: usize, window: Duration) -> u32 {\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 flooding PTY sessions the brain will resume.\r\n    let mut conns = Vec::new();\r\n    let mut pids = Vec::new();\r\n    for i in 0..n_sessions {\r\n        let (_sid, pid, conn) = spawn_flood(&name, &format!(\"flood-ep-{i}\"));\r\n        pids.push(pid);\r\n        conns.push(conn); // hold the spawner controller conns open (undrained)\r\n    }\r\n    // Let the floods get going so the brain conn genuinely backs up on subscribe.\r\n    thread::sleep(Duration::from_millis(600));\r\n\r\n    let ticks = Arc::new(AtomicU32::new(0));\r\n    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n    run_heartbeat_probe(&name, mode, Arc::clone(&ticks), Arc::clone(&stop));\r\n\r\n    thread::sleep(window);\r\n    stop.store(true, Ordering::Relaxed);\r\n    let count = ticks.load(Ordering::Relaxed);\r\n\r\n    for pid in pids {\r\n        kill_pid(pid);\r\n    }\r\n    drop(conns);\r\n    count\r\n}\r\n\r\n/// STEADY-STATE severity — the SHIPPING path: the daemon brain resumes CURSOR-ONLY\r\n/// (`resume_session_cursors`, no subscribe) while N sessions actively stream, so its\r\n/// heartbeat stays live. NO dead peer anywhere. RED-first: revert run_brain to the\r\n/// subscribing `resume_sessions` (or see the regression-guard test below) and this\r\n/// starves to a standstill.\r\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n#[test]\r\nfn daemon_cursor_only_resume_keeps_heartbeat_live_steady_state() {\r\n    const N: usize = 6;\r\n    let window = Duration::from_secs(6);\r\n    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\r\n    // Healthy heartbeat is ~ms per tick → hundreds per window; the RED path lands ~0.\r\n    assert!(\r\n        ticks >= 100,\r\n        \"the daemon's cursor-only resume must keep the heartbeat live under a {N}-session \\\r\n         flood (got {ticks} ticks in {window:?}). A low count means the SharedSend \\\r\n         self-deadlock re-appeared — the brain is subscribing a conn it does not drain.\"\r\n    );\r\n}\r\n\r\n/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\r\n/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\r\n/// rate (the sessions predate this generation). The new generation's heartbeat — hence\r\n/// its readiness/promotion — is never starved.\r\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n#[test]\r\nfn daemon_cursor_only_resume_keeps_heartbeat_live_respawn_interleave() {\r\n    const N: usize = 6;\r\n    let window = Duration::from_secs(6);\r\n    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\r\n    assert!(\r\n        ticks >= 100,\r\n        \"a respawning brain's cursor-only resume must keep its heartbeat live while N={N} \\\r\n         sessions flood (got {ticks} ticks in {window:?}) — else the reply-starvation \\\r\n         deadlock stalls the new generation's readiness/promotion.\"\r\n    );\r\n}\r\n\r\n/// STRUCTURAL zero-subscriber resume guard — REPLACES the retired best-of-K flood-\r\n/// starvation guard (`nondraining_subscriber_starves_under_flood_regression_guard`),\r\n/// which went stale after the r4 Option-C fix bounded every physical-conn write (the\r\n/// `BrokerConn` watchdog retires a blocked write at `brain_write_deadline()`), so a\r\n/// non-draining subscriber no longer starves the heartbeat to ~0 and the timing-based\r\n/// 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    drop(hazard_brain);\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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\brain_resume_conn_deadlock.rs"}}},"isError":false,"timestamp":1784195778077}}
{"type":"message","id":"02cf08d1","parentId":"214a3092","timestamp":"2026-07-16T09:56:18.081Z","message":{"role":"toolResult","toolCallId":"call_xwVmvMU59AbnZzMwkTnkKaCe|fc_0b42584a8ab0545d016a58aac207088199b07f1645c7e43b80","toolName":"read","content":[{"type":"text","text":"//! THE-FORKENING W4 — the bare `spt update` composite, end to end\n//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\n//! release, THEN updates a registered gh_release adapter — core-first order,\n//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\n//! sibling `[[bin]]` copied to `gh(.exe)` on a PATH-prepended dir) serves the\n//! whole release channel from a local fixture dir, so the mandated gh carrier\n//! runs for real — subprocess, argv shapes, asset staging — with no network\n//! and no real gh auth. The trust anchor is a test key overlaid via\n//! `release-keys.json`; the core apply is the daemonless path (no daemon in\n//! this home), which swaps THIS test's private copy of the spt binary (never\n//! the shared target/debug artifact).\n//!\n//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\n//! Windows' installer-detection heuristic forces UAC elevation on a\n//! manifest-less exe whose name contains it (os error 740; the\n//! post_step_fixture precedent).\n//\n// [int->REQ-UPDATE-DEFAULT-COMPOSITE]\n\nuse ed25519_dalek::{Signer, SigningKey};\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\nuse std::process::{Command, Output};\nuse std::time::Duration;\n\nmod common;\nuse common::CommandNoWindowExt;\n\nconst KEY_ID: &str = \"rel-composite-e2e\";\nconst NEW_CORE_BYTES: &[u8] = b\"NEW-CORE-BYTES-composite-e2e\";\n\nfn release_key() -> SigningKey {\n    SigningKey::from_bytes(&[7u8; 32])\n}\n\nfn hex(bytes: &[u8]) -> String {\n    bytes.iter().map(|b| format!(\"{b:02x}\")).collect()\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/// `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(\"spt update must complete within its deadline\")\n        .expect(\"run spt update\")\n}\n\n/// The signed update set the fake channel serves: one artifact for the CURRENT\n/// platform (fetch requires `asset_name`), product 9.9.9, docs-less.\nfn signed_set_json(version: u64, artifact: &[u8]) -> String {\n    let meta = spt_daemon::UpdateSetMetadata {\n        version,\n        channel: \"stable\".to_string(),\n        expires_at_ms: u64::MAX,\n        key_id: KEY_ID.to_string(),\n        artifacts: BTreeMap::from([(\n            spt_daemon::current_platform().to_string(),\n            spt_daemon::UpdateArtifactMetadata {\n                artifact_sha256: spt_daemon::sha256_hex(artifact),\n                brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n                broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,\n                asset_name: Some(\"spt-composite-artifact\".to_string()),\n            },\n        )]),\n        provenance: None,\n        product_version: \"9.9.9\".to_string(),\n        docs: None,\n    };\n    let metadata_json = serde_json::to_string(&meta).unwrap();\n    let sig = release_key().sign(metadata_json.as_bytes());\n    serde_json::to_string(&spt_daemon::SignedUpdateSet {\n        metadata_json,\n        signature_hex: hex(&sig.to_bytes()),\n    })\n    .unwrap()\n}\n\n/// Build a `.spt` adapter release archive (tar, relative members — the exact\n/// shape `extract_release_archive` consumes) whose root manifest declares\n/// `version` and keeps the gh_release `[update]` block (so the re-register\n/// after the swap preserves the avenue).\nfn adapter_spt(scratch: &Path, version: &str) -> Vec<u8> {\n    let tree = scratch.join(format!(\"adapter-tree-{version}\"));\n    std::fs::create_dir_all(&tree).unwrap();\n    std::fs::write(tree.join(\"manifest.toml\"), adapter_manifest(version)).unwrap();\n    let out = scratch.join(format!(\"adapter-{version}.spt\"));\n    let st = Command::new(\"tar\")\n        .arg(\"-cf\")\n        .arg(&out)\n        .arg(\"-C\")\n        .arg(&tree)\n        .arg(\".\")\n        .status()\n        .expect(\"system tar (present on Win10+ and Linux)\");\n    assert!(st.success(), \"tar the adapter tree\");\n    std::fs::read(&out).unwrap()\n}\n\nfn adapter_manifest(version: &str) -> String {\n    format!(\n        \"[adapter]\\nname = \\\"cc\\\"\\nkind = \\\"harness\\\"\\nversion = \\\"{version}\\\"\\n\\\n         min_spt_core_version = \\\"0\\\"\\n\\n\\\n         [update]\\navenue = \\\"gh_release\\\"\\nrepo = \\\"u/r\\\"\\n\"\n    )\n}\n\n#[test]\nfn bare_update_applies_core_then_updates_adapter_in_one_invocation() {\n    // ── (1) Isolated home + this test's PRIVATE copy of the spt binary (the\n    //    core apply swaps `current_exe` — driving the shared target/debug\n    //    binary would clobber it for the rest of the suite). The fake gh sits\n    //    beside it on a PATH-prepended dir. ──\n    let root = tempfile::tempdir().unwrap();\n    let home = root.path().join(\"home\");\n    let identity = home.join(\"identity\");\n    std::fs::create_dir_all(&identity).unwrap();\n    std::fs::write(\n        identity.join(\"release-keys.json\"),\n        serde_json::json!({ \"keys\": { KEY_ID: hex(release_key().verifying_key().as_bytes()) } })\n            .to_string(),\n    )\n    .unwrap();\n\n    let bindir = root.path().join(\"bin\");\n    std::fs::create_dir_all(&bindir).unwrap();\n    let spt = bindir.join(if cfg!(windows) { \"spt.exe\" } else { \"spt\" });\n    std::fs::copy(env!(\"CARGO_BIN_EXE_spt\"), &spt).unwrap();\n    let gh = bindir.join(if cfg!(windows) { \"gh.exe\" } else { \"gh\" });\n    std::fs::copy(sibling_bin(\"gh_fixture\"), &gh).expect(\"gh_fixture must be built\");\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        for b in [&spt, &gh] {\n            let mut p = std::fs::metadata(b).unwrap().permissions();\n            p.set_mode(0o755);\n            std::fs::set_permissions(b, p).unwrap();\n        }\n    }\n    let path_env = format!(\n        \"{}{}{}\",\n        bindir.display(),\n        if cfg!(windows) { \";\" } else { \":\" },\n        std::env::var(\"PATH\").unwrap_or_default()\n    );\n\n    // ── (2) The fake release channel: the CORE set (update-set.json + the\n    //    platform artifact) and the ADAPTER release (latest tag v1.1.0 +\n    //    adapter.spt with the bumped manifest). ──\n    let gh_root = root.path().join(\"gh-root\");\n    let assets = gh_root.join(\"assets\");\n    std::fs::create_dir_all(&assets).unwrap();\n    std::fs::write(gh_root.join(\"tag.txt\"), \"v1.1.0\").unwrap();\n    std::fs::write(assets.join(\"update-set.json\"), signed_set_json(1, NEW_CORE_BYTES)).unwrap();\n    std::fs::write(assets.join(\"spt-composite-artifact\"), NEW_CORE_BYTES).unwrap();\n    std::fs::write(assets.join(\"adapter.spt\"), adapter_spt(root.path(), \"1.1.0\")).unwrap();\n    let gh_log = root.path().join(\"gh.log\");\n\n    // ── (3) Register the gh_release adapter `cc` at 1.0.0 (its install dir is\n    //    the registered source_dir — the in-place update target). ──\n    std::env::set_var(\"SPT_HOME\", &home);\n    let install_dir = spt_store::perch::spt_home().join(\"srcs\").join(\"cc\");\n    std::fs::create_dir_all(&install_dir).unwrap();\n    std::fs::write(install_dir.join(\"manifest.toml\"), adapter_manifest(\"1.0.0\")).unwrap();\n    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &install_dir, 1000)\n        .unwrap();\n    std::env::remove_var(\"SPT_HOME\");\n\n    // ── (4) ONE invocation: bare `spt update`. ──\n    let out = {\n        let mut cmd = Command::new(&spt);\n        cmd.no_window()\n            .arg(\"update\")\n            .env(\"SPT_HOME\", &home)\n            .env(\"PATH\", &path_env)\n            .env(\"SPT_FAKE_GH_ROOT\", &gh_root)\n            .env(\"SPT_FAKE_GH_LOG\", &gh_log);\n        output_bounded(cmd, Duration::from_secs(120))\n    };\n    let stdout = String::from_utf8_lossy(&out.stdout).to_string();\n    let stderr = String::from_utf8_lossy(&out.stderr).to_string();\n    let log = std::fs::read_to_string(&gh_log).unwrap_or_default();\n    eprintln!(\n        \"=== composite: ok={} ===\\n--- stdout ---\\n{stdout}\\n--- stderr ---\\n{stderr}\\n--- gh log ---\\n{log}\",\n        out.status.success()\n    );\n\n    assert!(\n        out.status.success(),\n        \"bare `spt update` must exit 0 when both legs succeed\"\n    );\n\n    // ── (5) CORE leg landed: the private spt copy now holds the NEW bytes\n    //    (daemonless swap — no daemon in this home), reported as installed. ──\n    assert_eq!(\n        std::fs::read(&spt).unwrap(),\n        NEW_CORE_BYTES,\n        \"the core apply must swap the invoking binary's on-disk seat\"\n    );\n    assert!(\n        stdout.contains(\"Installed spt-core v9.9.9 on disk\"),\n        \"the daemonless applied notice names the product version: {stdout}\"\n    );\n\n    // ── (6) ADAPTERS leg landed IN THE SAME INVOCATION: cc pulled 1.0.0 →\n    //    1.1.0 through the fake channel and re-registered. ──\n    assert!(\n        stderr.contains(\"ADAPTER_UPDATE_DONE:cc\"),\n        \"the adapter update must apply: {stderr}\"\n    );\n    assert!(\n        stderr.contains(\"ADAPTER_UPDATE_SUMMARY:cc: updated 1.0.0 -> 1.1.0\"),\n        \"the per-adapter summary line reports the pull: {stderr}\"\n    );\n    let landed = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).unwrap();\n    assert!(\n        landed.contains(\"version = \\\"1.1.0\\\"\"),\n        \"the registered install dir must hold the new manifest: {landed}\"\n    );\n\n    // ── (7) CORE-FIRST order (the composite doctrine): the set download\n    //    happens strictly before the adapter's latest-release query. ──\n    let set_at = log\n        .find(\"--pattern update-set.json\")\n        .expect(\"the core leg downloaded the update set via gh\");\n    let adapter_at = log\n        .find(\"api repos/u/r/releases/latest\")\n        .expect(\"the adapters leg queried the adapter release via gh\");\n    assert!(\n        set_at < adapter_at,\n        \"core leg must run before the adapters leg (core-first): \\n{log}\"\n    );\n}\n"}],"details":{"displayContent":{"text":"//! THE-FORKENING W4 — the bare `spt update` composite, end to end\r\n//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\r\n//! release, THEN updates a registered gh_release adapter — core-first order,\r\n//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\r\n//! sibling `[[bin]]` copied to `gh(.exe)` on a PATH-prepended dir) serves the\r\n//! whole release channel from a local fixture dir, so the mandated gh carrier\r\n//! runs for real — subprocess, argv shapes, asset staging — with no network\r\n//! and no real gh auth. The trust anchor is a test key overlaid via\r\n//! `release-keys.json`; the core apply is the daemonless path (no daemon in\r\n//! this home), which swaps THIS test's private copy of the spt binary (never\r\n//! the shared target/debug artifact).\r\n//!\r\n//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\r\n//! Windows' installer-detection heuristic forces UAC elevation on a\r\n//! manifest-less exe whose name contains it (os error 740; the\r\n//! post_step_fixture precedent).\r\n//\r\n// [int->REQ-UPDATE-DEFAULT-COMPOSITE]\r\n\r\nuse ed25519_dalek::{Signer, SigningKey};\r\nuse std::collections::BTreeMap;\r\nuse std::path::{Path, PathBuf};\r\nuse std::process::{Command, Output};\r\nuse std::time::Duration;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\nconst KEY_ID: &str = \"rel-composite-e2e\";\r\nconst NEW_CORE_BYTES: &[u8] = b\"NEW-CORE-BYTES-composite-e2e\";\r\n\r\nfn release_key() -> SigningKey {\r\n    SigningKey::from_bytes(&[7u8; 32])\r\n}\r\n\r\nfn hex(bytes: &[u8]) -> String {\r\n    bytes.iter().map(|b| format!(\"{b:02x}\")).collect()\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/// `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(\"spt update must complete within its deadline\")\r\n        .expect(\"run spt update\")\r\n}\r\n\r\n/// The signed update set the fake channel serves: one artifact for the CURRENT\r\n/// platform (fetch requires `asset_name`), product 9.9.9, docs-less.\r\nfn signed_set_json(version: u64, artifact: &[u8]) -> String {\r\n    let meta = spt_daemon::UpdateSetMetadata {\r\n        version,\r\n        channel: \"stable\".to_string(),\r\n        expires_at_ms: u64::MAX,\r\n        key_id: KEY_ID.to_string(),\r\n        artifacts: BTreeMap::from([(\r\n            spt_daemon::current_platform().to_string(),\r\n            spt_daemon::UpdateArtifactMetadata {\r\n                artifact_sha256: spt_daemon::sha256_hex(artifact),\r\n                brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\r\n                broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,\r\n                asset_name: Some(\"spt-composite-artifact\".to_string()),\r\n            },\r\n        )]),\r\n        provenance: None,\r\n        product_version: \"9.9.9\".to_string(),\r\n        docs: None,\r\n    };\r\n    let metadata_json = serde_json::to_string(&meta).unwrap();\r\n    let sig = release_key().sign(metadata_json.as_bytes());\r\n    serde_json::to_string(&spt_daemon::SignedUpdateSet {\r\n        metadata_json,\r\n        signature_hex: hex(&sig.to_bytes()),\r\n    })\r\n    .unwrap()\r\n}\r\n\r\n/// Build a `.spt` adapter release archive (tar, relative members — the exact\r\n/// shape `extract_release_archive` consumes) whose root manifest declares\r\n/// `version` and keeps the gh_release `[update]` block (so the re-register\r\n/// after the swap preserves the avenue).\r\nfn adapter_spt(scratch: &Path, version: &str) -> Vec<u8> {\r\n    let tree = scratch.join(format!(\"adapter-tree-{version}\"));\r\n    std::fs::create_dir_all(&tree).unwrap();\r\n    std::fs::write(tree.join(\"manifest.toml\"), adapter_manifest(version)).unwrap();\r\n    let out = scratch.join(format!(\"adapter-{version}.spt\"));\r\n    let st = Command::new(\"tar\")\r\n        .arg(\"-cf\")\r\n        .arg(&out)\r\n        .arg(\"-C\")\r\n        .arg(&tree)\r\n        .arg(\".\")\r\n        .status()\r\n        .expect(\"system tar (present on Win10+ and Linux)\");\r\n    assert!(st.success(), \"tar the adapter tree\");\r\n    std::fs::read(&out).unwrap()\r\n}\r\n\r\nfn adapter_manifest(version: &str) -> String {\r\n    format!(\r\n        \"[adapter]\\nname = \\\"cc\\\"\\nkind = \\\"harness\\\"\\nversion = \\\"{version}\\\"\\n\\\r\n         min_spt_core_version = \\\"0\\\"\\n\\n\\\r\n         [update]\\navenue = \\\"gh_release\\\"\\nrepo = \\\"u/r\\\"\\n\"\r\n    )\r\n}\r\n\r\n#[test]\r\nfn bare_update_applies_core_then_updates_adapter_in_one_invocation() {\r\n    // ── (1) Isolated home + this test's PRIVATE copy of the spt binary (the\r\n    //    core apply swaps `current_exe` — driving the shared target/debug\r\n    //    binary would clobber it for the rest of the suite). The fake gh sits\r\n    //    beside it on a PATH-prepended dir. ──\r\n    let root = tempfile::tempdir().unwrap();\r\n    let home = root.path().join(\"home\");\r\n    let identity = home.join(\"identity\");\r\n    std::fs::create_dir_all(&identity).unwrap();\r\n    std::fs::write(\r\n        identity.join(\"release-keys.json\"),\r\n        serde_json::json!({ \"keys\": { KEY_ID: hex(release_key().verifying_key().as_bytes()) } })\r\n            .to_string(),\r\n    )\r\n    .unwrap();\r\n\r\n    let bindir = root.path().join(\"bin\");\r\n    std::fs::create_dir_all(&bindir).unwrap();\r\n    let spt = bindir.join(if cfg!(windows) { \"spt.exe\" } else { \"spt\" });\r\n    std::fs::copy(env!(\"CARGO_BIN_EXE_spt\"), &spt).unwrap();\r\n    let gh = bindir.join(if cfg!(windows) { \"gh.exe\" } else { \"gh\" });\r\n    std::fs::copy(sibling_bin(\"gh_fixture\"), &gh).expect(\"gh_fixture must be built\");\r\n    #[cfg(unix)]\r\n    {\r\n        use std::os::unix::fs::PermissionsExt;\r\n        for b in [&spt, &gh] {\r\n            let mut p = std::fs::metadata(b).unwrap().permissions();\r\n            p.set_mode(0o755);\r\n            std::fs::set_permissions(b, p).unwrap();\r\n        }\r\n    }\r\n    let path_env = format!(\r\n        \"{}{}{}\",\r\n        bindir.display(),\r\n        if cfg!(windows) { \";\" } else { \":\" },\r\n        std::env::var(\"PATH\").unwrap_or_default()\r\n    );\r\n\r\n    // ── (2) The fake release channel: the CORE set (update-set.json + the\r\n    //    platform artifact) and the ADAPTER release (latest tag v1.1.0 +\r\n    //    adapter.spt with the bumped manifest). ──\r\n    let gh_root = root.path().join(\"gh-root\");\r\n    let assets = gh_root.join(\"assets\");\r\n    std::fs::create_dir_all(&assets).unwrap();\r\n    std::fs::write(gh_root.join(\"tag.txt\"), \"v1.1.0\").unwrap();\r\n    std::fs::write(assets.join(\"update-set.json\"), signed_set_json(1, NEW_CORE_BYTES)).unwrap();\r\n    std::fs::write(assets.join(\"spt-composite-artifact\"), NEW_CORE_BYTES).unwrap();\r\n    std::fs::write(assets.join(\"adapter.spt\"), adapter_spt(root.path(), \"1.1.0\")).unwrap();\r\n    let gh_log = root.path().join(\"gh.log\");\r\n\r\n    // ── (3) Register the gh_release adapter `cc` at 1.0.0 (its install dir is\r\n    //    the registered source_dir — the in-place update target). ──\r\n    std::env::set_var(\"SPT_HOME\", &home);\r\n    let install_dir = spt_store::perch::spt_home().join(\"srcs\").join(\"cc\");\r\n    std::fs::create_dir_all(&install_dir).unwrap();\r\n    std::fs::write(install_dir.join(\"manifest.toml\"), adapter_manifest(\"1.0.0\")).unwrap();\r\n    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &install_dir, 1000)\r\n        .unwrap();\r\n    std::env::remove_var(\"SPT_HOME\");\r\n\r\n    // ── (4) ONE invocation: bare `spt update`. ──\r\n    let out = {\r\n        let mut cmd = Command::new(&spt);\r\n        cmd.no_window()\r\n            .arg(\"update\")\r\n            .env(\"SPT_HOME\", &home)\r\n            .env(\"PATH\", &path_env)\r\n            .env(\"SPT_FAKE_GH_ROOT\", &gh_root)\r\n            .env(\"SPT_FAKE_GH_LOG\", &gh_log);\r\n        output_bounded(cmd, Duration::from_secs(120))\r\n    };\r\n    let stdout = String::from_utf8_lossy(&out.stdout).to_string();\r\n    let stderr = String::from_utf8_lossy(&out.stderr).to_string();\r\n    let log = std::fs::read_to_string(&gh_log).unwrap_or_default();\r\n    eprintln!(\r\n        \"=== composite: ok={} ===\\n--- stdout ---\\n{stdout}\\n--- stderr ---\\n{stderr}\\n--- gh log ---\\n{log}\",\r\n        out.status.success()\r\n    );\r\n\r\n    assert!(\r\n        out.status.success(),\r\n        \"bare `spt update` must exit 0 when both legs succeed\"\r\n    );\r\n\r\n    // ── (5) CORE leg landed: the private spt copy now holds the NEW bytes\r\n    //    (daemonless swap — no daemon in this home), reported as installed. ──\r\n    assert_eq!(\r\n        std::fs::read(&spt).unwrap(),\r\n        NEW_CORE_BYTES,\r\n        \"the core apply must swap the invoking binary's on-disk seat\"\r\n    );\r\n    assert!(\r\n        stdout.contains(\"Installed spt-core v9.9.9 on disk\"),\r\n        \"the daemonless applied notice names the product version: {stdout}\"\r\n    );\r\n\r\n    // ── (6) ADAPTERS leg landed IN THE SAME INVOCATION: cc pulled 1.0.0 →\r\n    //    1.1.0 through the fake channel and re-registered. ──\r\n    assert!(\r\n        stderr.contains(\"ADAPTER_UPDATE_DONE:cc\"),\r\n        \"the adapter update must apply: {stderr}\"\r\n    );\r\n    assert!(\r\n        stderr.contains(\"ADAPTER_UPDATE_SUMMARY:cc: updated 1.0.0 -> 1.1.0\"),\r\n        \"the per-adapter summary line reports the pull: {stderr}\"\r\n    );\r\n    let landed = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).unwrap();\r\n    assert!(\r\n        landed.contains(\"version = \\\"1.1.0\\\"\"),\r\n        \"the registered install dir must hold the new manifest: {landed}\"\r\n    );\r\n\r\n    // ── (7) CORE-FIRST order (the composite doctrine): the set download\r\n    //    happens strictly before the adapter's latest-release query. ──\r\n    let set_at = log\r\n        .find(\"--pattern update-set.json\")\r\n        .expect(\"the core leg downloaded the update set via gh\");\r\n    let adapter_at = log\r\n        .find(\"api repos/u/r/releases/latest\")\r\n        .expect(\"the adapters leg queried the adapter release via gh\");\r\n    assert!(\r\n        set_at < adapter_at,\r\n        \"core leg must run before the adapters leg (core-first): \\n{log}\"\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\composite_e2e.rs"}}},"isError":false,"timestamp":1784195778081}}
{"type":"message","id":"a64c9d22","parentId":"02cf08d1","timestamp":"2026-07-16T09:56:18.086Z","message":{"role":"toolResult","toolCallId":"call_dAgqmkZhli4yXkIDCsLWANT9|fc_0b42584a8ab0545d016a58aac207108199ba39943628452d03","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//! `cargo test --test brain_decouple` on Windows.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::AttachIntent;\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\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\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\n/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output case\n/// that fills a non-reading controller's socket and parks its writer.\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\n            \"/c\".to_string(),\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\n        ],\n    );\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\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 connect(name: &str) -> Stream {\n    for _ in 0..200 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Subscribe `conn` to `sid` with `intent` + `by`, returning the resolved outcome.\n/// Runs the read on a bounded thread so a wedge surfaces as a timeout, not a hang.\nfn subscribe(\n    name: &str,\n    sid: u64,\n    intent: AttachIntent,\n    by: Option<&str>,\n    deadline: Duration,\n) -> Option<SubscribeOutcome> {\n    let name = name.to_string();\n    let by = by.map(str::to_string);\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = match LocalSocketTransport::connect(&name) {\n            Ok(mut c) => {\n                if send_hello(&mut c, Role::Brain).is_err() {\n                    let _ = tx.send(None);\n                    return;\n                }\n                c\n            }\n            Err(_) => {\n                let _ = tx.send(None);\n                return;\n            }\n        };\n        send(\n            &mut c,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent,\n                by,\n            })\n            .unwrap(),\n        );\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => {\n                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\n                    // Keep the conn briefly so a Controller take is not immediately\n                    // detached (drop would clear the slot) — then let it fall.\n                    thread::sleep(Duration::from_millis(200));\n                    let _ = tx.send(Some(reply.outcome));\n                    return;\n                }\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = tx.send(None);\n                    return;\n                }\n            }\n        }\n    });\n    rx.recv_timeout(deadline).ok().flatten()\n}\n\n/// Query the broker's stall-evict tally.\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n    let name = name.to_string();\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n                    let _ = tx.send(r.count);\n                    return;\n                }\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = tx.send(0);\n                    return;\n                }\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// UPDATE-WEDGE round-4 — PHYSICAL writer/connection retirement gate.\n//\n// The companion test above proves LOGICAL isolation: a wedged controller is\n// stall-evicted (the ROLE is freed), the viewer keeps ticking, a fresh take\n// completes. But hertz's RCA (`docs/UPDATE-WEDGE-ROOT-CAUSE.md` @ f8596ca,\n// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\n// connection: `stall_evict_controller` drops the `ControllerSink` (one\n// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\n// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\n// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\n// wedged inside `write_frame` (WriteFileEx + SleepEx(INFINITE, alertable) — no\n// supported write timeout) until the client drops or the process exits (~128 s\n// in the field capture). The `handle_conn` reader for that same conn also never\n// reaches EOF while the client end stays open. This is LOAD-GATED: it needs a\n// real seq>0 controller frame plus a non-draining consumer (seq-0 boot + a\n// quiescent update are clean).\n//\n// This test reproduces that EXACT Windows failure mode with a real PTY child and\n// a genuinely black-holed controller connection, then asserts the two things the\n// logical gate cannot see:\n//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\n//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\n//     client ever dropping its end;                                    ← RED pre-fix\n//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\n// Post-fix (conn.rs bounded/cancelable/poison-on-failure framed write): the\n// absolute deadline fires out of band, poisons the whole physical conn, aborts\n// read+write so `handle_conn` reaches EOF cleanup, and a fresh controller resumes\n// from the frozen cursor with NO lost frame.\n//\n// Every read anywhere in this test rides a bounded thread+channel or a polled\n// deadline; the test captures plain values (bools / Options / Instants) as it\n// goes and only asserts AFTER teardown, so it can never hang even on broken code\n// — worst case it fails an assert after its bounded windows elapse.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/// A real PTY child (PowerShell) that emits `SEED`, parks on a first `Read-Host`,\n/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n/// and parks again. The seq-0 boot output (VT init + prompt + SEED) gives a real\n/// `seq>0` controller frame; the burst floods a non-draining controller's socket\n/// so its writer parks INSIDE `write_frame` — the exact black-hole the RCA pinned.\n/// Sibling of [`flood_spawn_req`], but FINITE + input-triggered (the flood child\n/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\n#[cfg(windows)]\nfn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n        program: \"powershell\".to_string(),\n        args: vec![\n            \"-NoProfile\".to_string(),\n            \"-Command\".to_string(),\n            \"Write-Output SEED; $null = Read-Host; \\\n             foreach ($i in 1..1400) { Write-Output ('B' * 1023) }; $null = Read-Host\"\n                .to_string(),\n        ],\n        rows: 24,\n        cols: 80,\n        endpoint: endpoint.to_string(),\n…\n    }\n}\n\n[Showing lines 1-433 of 834. Use :434 to continue]"}],"details":{"truncation":{"content":"//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\r\n//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\r\n//!\r\n//! ## The bug this gate locks down (the update-wedge root)\r\n//!\r\n//! A controller's writer thread does a BLOCKING socket write to its brain\r\n//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\r\n//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\r\n//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\r\n//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\r\n//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\r\n//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\r\n//! `daemon status` still read healthy.\r\n//!\r\n//! ## Faithful in-process analog of a suspended brain\r\n//!\r\n//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\r\n//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\r\n//! buffer, the controller channel fills, and A's writer thread parks INSIDE\r\n//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\r\n//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\r\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\r\n//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\r\n//! it never relaxes an assertion).\r\n//!\r\n//! ## What this rig asserts (exactly four — the decouple invariant)\r\n//!\r\n//! With the wedged controller's writer parked past the brain-write deadline:\r\n//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\r\n//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\r\n//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\r\n//!    busy-refused forever);\r\n//! 3. that take resolves PROMPTLY (well under the watchdog — a stall-evict, not a hang);\r\n//! 4. the stall-evict is OBSERVABLE (the broker-global tally increments, the datum\r\n//!    `daemon status` surfaces).\r\n//!\r\n//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\r\n//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\r\n//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\r\n//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\r\n//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\r\n//! real brain child remains the MANUAL field rig from the incident night — this CI test\r\n//! is the deterministic in-process ANALOG, not that run.\r\n//!\r\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\r\n//!\r\n//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\r\n//! spins a REAL broker + PTY child. Run isolated with\r\n//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\r\n//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\r\n//! `cargo test --test brain_decouple` on Windows.\r\n\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::{Arc, OnceLock};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse interprocess::local_socket::Stream;\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{\r\n    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\r\n    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\r\n};\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\r\nuse spt_net::net::attach::AttachIntent;\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-braindecouple-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\r\nfn init_home() {\r\n    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\n/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output case\r\n/// that fills a non-reading controller's socket and parks its writer.\r\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\r\n            \"/c\".to_string(),\r\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\r\n        ],\r\n    );\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\nfn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\r\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\r\n}\r\n\r\nfn connect(name: &str) -> Stream {\r\n    for _ in 0..200 {\r\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\r\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\r\n            return c;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"could not connect to broker {name}\");\r\n}\r\n\r\n/// Subscribe `conn` to `sid` with `intent` + `by`, returning the resolved outcome.\r\n/// Runs the read on a bounded thread so a wedge surfaces as a timeout, not a hang.\r\nfn subscribe(\r\n    name: &str,\r\n    sid: u64,\r\n    intent: AttachIntent,\r\n    by: Option<&str>,\r\n    deadline: Duration,\r\n) -> Option<SubscribeOutcome> {\r\n    let name = name.to_string();\r\n    let by = by.map(str::to_string);\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    thread::spawn(move || {\r\n        let mut c = match LocalSocketTransport::connect(&name) {\r\n            Ok(mut c) => {\r\n                if send_hello(&mut c, Role::Brain).is_err() {\r\n                    let _ = tx.send(None);\r\n                    return;\r\n                }\r\n                c\r\n            }\r\n            Err(_) => {\r\n                let _ = tx.send(None);\r\n                return;\r\n            }\r\n        };\r\n        send(\r\n            &mut c,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent,\r\n                by,\r\n            })\r\n            .unwrap(),\r\n        );\r\n        loop {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == KIND_SUBSCRIBED => {\r\n                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\r\n                    // Keep the conn briefly so a Controller take is not immediately\r\n                    // detached (drop would clear the slot) — then let it fall.\r\n                    thread::sleep(Duration::from_millis(200));\r\n                    let _ = tx.send(Some(reply.outcome));\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => {\r\n                    let _ = tx.send(None);\r\n                    return;\r\n                }\r\n            }\r\n        }\r\n    });\r\n    rx.recv_timeout(deadline).ok().flatten()\r\n}\r\n\r\n/// Query the broker's stall-evict tally.\r\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\r\n    let name = name.to_string();\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    thread::spawn(move || {\r\n        let mut c = connect(&name);\r\n        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\r\n        loop {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\r\n                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\r\n                    let _ = tx.send(r.count);\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => {\r\n                    let _ = tx.send(0);\r\n                    return;\r\n                }\r\n            }\r\n        }\r\n    });\r\n    rx.recv_timeout(deadline).unwrap_or(0)\r\n}\r\n\r\n// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\r\n#[test]\r\nfn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\r\n    init_home();\r\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\r\n    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\r\n    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\r\n    // stall-evict can free the slot in the window this test acts in.\r\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\r\n\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    // ── Spawner A: spawn the flood child (dispatch_spawn pre-attaches A as the\r\n    //    LOCAL controller, by=None). ──\r\n    let mut a = connect(&name);\r\n    send(\r\n        &mut a,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(flood_spawn_req(\"decouple-ep\")).unwrap(),\r\n    );\r\n    let sid = loop {\r\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\r\n        }\r\n    };\r\n\r\n    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\r\n    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\r\n    //    brain analog. R MUST be a remote controller: a second remote's Control\r\n    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\r\n    //    (the wedge), so only the stall-evict can free the slot — this is what\r\n    //    isolates the fix from the pre-existing \"a remote takes an UNDRIVEN\r\n    //    (local-only) session\" silent-control path. R's conn is held open (never\r\n    //    read from) on its own thread so the broker's writer for R parks in\r\n    //    write_frame exactly as against a suspended brain. ──\r\n    let rname = name.clone();\r\n    thread::spawn(move || {\r\n        let mut r = connect(&rname);\r\n        send(\r\n            &mut r,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Control,\r\n                by: Some(\"operator-one\".to_string()),\r\n            })\r\n            .unwrap(),\r\n        );\r\n        // Read ONLY the subscribed reply, then go silent forever holding the conn.\r\n        loop {\r\n            match read_frame(&mut r) {\r\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\r\n                Ok(_) => continue,\r\n                Err(_) => return,\r\n            }\r\n        }\r\n        // Hold the conn open, never draining — the broker's R-writer wedges on the\r\n        // flood. Park long past the test; the process-per-test exit reaps it.\r\n        thread::sleep(Duration::from_secs(120));\r\n        drop(r);\r\n    });\r\n    // Give R time to become the controller and its writer to wedge on the flood.\r\n    thread::sleep(Duration::from_millis(300));\r\n    // The local spawner is displaced by R's take; drop it (no longer the controller).\r\n    drop(a);\r\n\r\n    // ── Viewer V: a SEPARATE conn that DOES drain. Prove the fan-out keeps ticking\r\n    //    to V while A's writer is wedged (the drain never blocks on the wedged conn). ──\r\n    let vname = name.clone();\r\n    let v_frames = Arc::new(std::sync::atomic::AtomicU64::new(0));\r\n    let v_frames_w = Arc::clone(&v_frames);\r\n    thread::spawn(move || {\r\n        let mut v = connect(&vname);\r\n        send(\r\n            &mut v,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Viewer,\r\n                by: Some(\"viewer-node\".to_string()),\r\n            })\r\n            .unwrap(),\r\n        );\r\n        while let Ok(f) = read_frame(&mut v) {\r\n            if f.kind != KIND_SUBSCRIBED {\r\n                v_frames_w.fetch_add(1, Ordering::Relaxed);\r\n            }\r\n        }\r\n    });\r\n\r\n    // Let A's writer wedge past the (shrunk) brain-write deadline, and let the viewer\r\n    // accumulate frames during the wedge.\r\n    thread::sleep(Duration::from_millis(900));\r\n    let ticks_during_wedge = v_frames.load(Ordering::Relaxed);\r\n\r\n    // ── The take: a DIFFERENT operator's Control subscribe. Pre-W2 the wedged\r\n    //    incumbent (A) is present → this busy-refuses forever. With the W2\r\n    //    stall-evict, A is dropped broker-side and B takes the freed slot. ──\r\n    let start = Instant::now();\r\n    let outcome = subscribe(\r\n        &name,\r\n        sid,\r\n        AttachIntent::Control,\r\n        Some(\"operator-two\"),\r\n        Duration::from_secs(10),\r\n    );\r\n    let took = start.elapsed();\r\n\r\n    let evicts = stall_evict_count(&name, Duration::from_secs(5));\r\n\r\n    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n\r\n    eprintln!(\r\n        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\r\n         take_elapsed={took:?} stall_evicts={evicts} ===\"\r\n    );\r\n\r\n    // The viewer kept ticking while the controller conn was wedged.\r\n    assert!(\r\n        ticks_during_wedge > 0,\r\n        \"an attached viewer must keep receiving output while the controller conn is \\\r\n         wedged — the fan-out must never block on the stalled brain conn\"\r\n    );\r\n    // The take COMPLETED and took control (the wedged incumbent was stall-evicted).\r\n    assert!(\r\n        matches!(\r\n            outcome,\r\n            Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)\r\n        ),\r\n        \"a second operator's Control subscribe must COMPLETE by stall-evicting the \\\r\n         wedged incumbent (pre-W2: busy-refused forever). got {outcome:?}\"\r\n    );\r\n    // It resolved WELL under the 10 s watchdog (the stall-evict is prompt, not a hang).\r\n    assert!(\r\n        took < Duration::from_secs(9),\r\n        \"the take must self-heal promptly, not ride the watchdog: {took:?}\"\r\n    );\r\n    // The stall-evict is observable (the broker-side release the daemon-status\r\n    // surface reports).\r\n    assert!(\r\n        evicts >= 1,\r\n        \"the stall-evict must be recorded in the broker-global tally (daemon-status \\\r\n         observability rider); got {evicts}\"\r\n    );\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// UPDATE-WEDGE round-4 — PHYSICAL writer/connection retirement gate.\r\n//\r\n// The companion test above proves LOGICAL isolation: a wedged controller is\r\n// stall-evicted (the ROLE is freed), the viewer keeps ticking, a fresh take\r\n// completes. But hertz's RCA (`docs/UPDATE-WEDGE-ROOT-CAUSE.md` @ f8596ca,\r\n// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\r\n// connection: `stall_evict_controller` drops the `ControllerSink` (one\r\n// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\r\n// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\r\n// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\r\n// wedged inside `write_frame` (WriteFileEx + SleepEx(INFINITE, alertable) — no\r\n// supported write timeout) until the client drops or the process exits (~128 s\r\n// in the field capture). The `handle_conn` reader for that same conn also never\r\n// reaches EOF while the client end stays open. This is LOAD-GATED: it needs a\r\n// real seq>0 controller frame plus a non-draining consumer (seq-0 boot + a\r\n// quiescent update are clean).\r\n//\r\n// This test reproduces that EXACT Windows failure mode with a real PTY child and\r\n// a genuinely black-holed controller connection, then asserts the two things the\r\n// logical gate cannot see:\r\n//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\r\n//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\r\n//     client ever dropping its end;                                    ← RED pre-fix\r\n//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\r\n// Post-fix (conn.rs bounded/cancelable/poison-on-failure framed write): the\r\n// absolute deadline fires out of band, poisons the whole physical conn, aborts\r\n// read+write so `handle_conn` reaches EOF cleanup, and a fresh controller resumes\r\n// from the frozen cursor with NO lost frame.\r\n//\r\n// Every read anywhere in this test rides a bounded thread+channel or a polled\r\n// deadline; the test captures plain values (bools / Options / Instants) as it\r\n// goes and only asserts AFTER teardown, so it can never hang even on broken code\r\n// — worst case it fails an assert after its bounded windows elapse.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\n/// A real PTY child (PowerShell) that emits `SEED`, parks on a first `Read-Host`,\r\n/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\r\n/// and parks again. The seq-0 boot output (VT init + prompt + SEED) gives a real\r\n/// `seq>0` controller frame; the burst floods a non-draining controller's socket\r\n/// so its writer parks INSIDE `write_frame` — the exact black-hole the RCA pinned.\r\n/// Sibling of [`flood_spawn_req`], but FINITE + input-triggered (the flood child\r\n/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\r\n#[cfg(windows)]\r\nfn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\r\n    SpawnReq {\r\n        program: \"powershell\".to_string(),\r\n        args: vec![\r\n            \"-NoProfile\".to_string(),\r\n            \"-Command\".to_string(),\r\n            \"Write-Output SEED; $null = Read-Host; \\\r\n             foreach ($i in 1..1400) { Write-Output ('B' * 1023) }; $null = Read-Host\"\r\n                .to_string(),\r\n        ],\r\n        rows: 24,\r\n        cols: 80,\r\n        endpoint: endpoint.to_string(),\r","truncated":true,"truncatedBy":"lines","totalLines":834,"totalBytes":19108,"outputLines":433,"outputBytes":19108,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\r\n//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\r\n//!\r\n//! ## The bug this gate locks down (the update-wedge root)\r\n//!\r\n//! A controller's writer thread does a BLOCKING socket write to its brain\r\n//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\r\n//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\r\n//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\r\n//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\r\n//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\r\n//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\r\n//! `daemon status` still read healthy.\r\n//!\r\n//! ## Faithful in-process analog of a suspended brain\r\n//!\r\n//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\r\n//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\r\n//! buffer, the controller channel fills, and A's writer thread parks INSIDE\r\n//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\r\n//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\r\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\r\n//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\r\n//! it never relaxes an assertion).\r\n//!\r\n//! ## What this rig asserts (exactly four — the decouple invariant)\r\n//!\r\n//! With the wedged controller's writer parked past the brain-write deadline:\r\n//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\r\n//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\r\n//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\r\n//!    busy-refused forever);\r\n//! 3. that take resolves PROMPTLY (well under the watchdog — a stall-evict, not a hang);\r\n//! 4. the stall-evict is OBSERVABLE (the broker-global tally increments, the datum\r\n//!    `daemon status` surfaces).\r\n//!\r\n//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\r\n//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\r\n//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\r\n//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\r\n//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\r\n//! real brain child remains the MANUAL field rig from the incident night — this CI test\r\n//! is the deterministic in-process ANALOG, not that run.\r\n//!\r\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\r\n//!\r\n//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\r\n//! spins a REAL broker + PTY child. Run isolated with\r\n//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\r\n//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\r\n//! `cargo test --test brain_decouple` on Windows.\r\n\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::{Arc, OnceLock};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse interprocess::local_socket::Stream;\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{\r\n    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\r\n    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\r\n};\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\r\nuse spt_net::net::attach::AttachIntent;\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-braindecouple-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\r\nfn init_home() {\r\n    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\n/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output case\r\n/// that fills a non-reading controller's socket and parks its writer.\r\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\r\n            \"/c\".to_string(),\r\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\r\n        ],\r\n    );\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\nfn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\r\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\r\n}\r\n\r\nfn connect(name: &str) -> Stream {\r\n    for _ in 0..200 {\r\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\r\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\r\n            return c;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"could not connect to broker {name}\");\r\n}\r\n\r\n/// Subscribe `conn` to `sid` with `intent` + `by`, returning the resolved outcome.\r\n/// Runs the read on a bounded thread so a wedge surfaces as a timeout, not a hang.\r\nfn subscribe(\r\n    name: &str,\r\n    sid: u64,\r\n    intent: AttachIntent,\r\n    by: Option<&str>,\r\n    deadline: Duration,\r\n) -> Option<SubscribeOutcome> {\r\n    let name = name.to_string();\r\n    let by = by.map(str::to_string);\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    thread::spawn(move || {\r\n        let mut c = match LocalSocketTransport::connect(&name) {\r\n            Ok(mut c) => {\r\n                if send_hello(&mut c, Role::Brain).is_err() {\r\n                    let _ = tx.send(None);\r\n                    return;\r\n                }\r\n                c\r\n            }\r\n            Err(_) => {\r\n                let _ = tx.send(None);\r\n                return;\r\n            }\r\n        };\r\n        send(\r\n            &mut c,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent,\r\n                by,\r\n            })\r\n            .unwrap(),\r\n        );\r\n        loop {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == KIND_SUBSCRIBED => {\r\n                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\r\n                    // Keep the conn briefly so a Controller take is not immediately\r\n                    // detached (drop would clear the slot) — then let it fall.\r\n                    thread::sleep(Duration::from_millis(200));\r\n                    let _ = tx.send(Some(reply.outcome));\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => {\r\n                    let _ = tx.send(None);\r\n                    return;\r\n                }\r\n            }\r\n        }\r\n    });\r\n    rx.recv_timeout(deadline).ok().flatten()\r\n}\r\n\r\n/// Query the broker's stall-evict tally.\r\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\r\n    let name = name.to_string();\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    thread::spawn(move || {\r\n        let mut c = connect(&name);\r\n        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\r\n        loop {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\r\n                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\r\n                    let _ = tx.send(r.count);\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => {\r\n                    let _ = tx.send(0);\r\n                    return;\r\n                }\r\n            }\r\n        }\r\n    });\r\n    rx.recv_timeout(deadline).unwrap_or(0)\r\n}\r\n\r\n// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\r\n#[test]\r\nfn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\r\n    init_home();\r\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\r\n    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\r\n    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\r\n    // stall-evict can free the slot in the window this test acts in.\r\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\r\n\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    // ── Spawner A: spawn the flood child (dispatch_spawn pre-attaches A as the\r\n    //    LOCAL controller, by=None). ──\r\n    let mut a = connect(&name);\r\n    send(\r\n        &mut a,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(flood_spawn_req(\"decouple-ep\")).unwrap(),\r\n    );\r\n    let sid = loop {\r\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\r\n        }\r\n    };\r\n\r\n    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\r\n    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\r\n    //    brain analog. R MUST be a remote controller: a second remote's Control\r\n    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\r\n    //    (the wedge), so only the stall-evict can free the slot — this is what\r\n    //    isolates the fix from the pre-existing \"a remote takes an UNDRIVEN\r\n    //    (local-only) session\" silent-control path. R's conn is held open (never\r\n    //    read from) on its own thread so the broker's writer for R parks in\r\n    //    write_frame exactly as against a suspended brain. ──\r\n    let rname = name.clone();\r\n    thread::spawn(move || {\r\n        let mut r = connect(&rname);\r\n        send(\r\n            &mut r,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Control,\r\n                by: Some(\"operator-one\".to_string()),\r\n            })\r\n            .unwrap(),\r\n        );\r\n        // Read ONLY the subscribed reply, then go silent forever holding the conn.\r\n        loop {\r\n            match read_frame(&mut r) {\r\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\r\n                Ok(_) => continue,\r\n                Err(_) => return,\r\n            }\r\n        }\r\n        // Hold the conn open, never draining — the broker's R-writer wedges on the\r\n        // flood. Park long past the test; the process-per-test exit reaps it.\r\n        thread::sleep(Duration::from_secs(120));\r\n        drop(r);\r\n    });\r\n    // Give R time to become the controller and its writer to wedge on the flood.\r\n    thread::sleep(Duration::from_millis(300));\r\n    // The local spawner is displaced by R's take; drop it (no longer the controller).\r\n    drop(a);\r\n\r\n    // ── Viewer V: a SEPARATE conn that DOES drain. Prove the fan-out keeps ticking\r\n    //    to V while A's writer is wedged (the drain never blocks on the wedged conn). ──\r\n    let vname = name.clone();\r\n    let v_frames = Arc::new(std::sync::atomic::AtomicU64::new(0));\r\n    let v_frames_w = Arc::clone(&v_frames);\r\n    thread::spawn(move || {\r\n        let mut v = connect(&vname);\r\n        send(\r\n            &mut v,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Viewer,\r\n                by: Some(\"viewer-node\".to_string()),\r\n            })\r\n            .unwrap(),\r\n        );\r\n        while let Ok(f) = read_frame(&mut v) {\r\n            if f.kind != KIND_SUBSCRIBED {\r\n                v_frames_w.fetch_add(1, Ordering::Relaxed);\r\n            }\r\n        }\r\n    });\r\n\r\n    // Let A's writer wedge past the (shrunk) brain-write deadline, and let the viewer\r\n    // accumulate frames during the wedge.\r\n    thread::sleep(Duration::from_millis(900));\r\n    let ticks_during_wedge = v_frames.load(Ordering::Relaxed);\r\n\r\n    // ── The take: a DIFFERENT operator's Control subscribe. Pre-W2 the wedged\r\n    //    incumbent (A) is present → this busy-refuses forever. With the W2\r\n    //    stall-evict, A is dropped broker-side and B takes the freed slot. ──\r\n    let start = Instant::now();\r\n    let outcome = subscribe(\r\n        &name,\r\n        sid,\r\n        AttachIntent::Control,\r\n        Some(\"operator-two\"),\r\n        Duration::from_secs(10),\r\n    );\r\n    let took = start.elapsed();\r\n\r\n    let evicts = stall_evict_count(&name, Duration::from_secs(5));\r\n\r\n    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n\r\n    eprintln!(\r\n        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\r\n         take_elapsed={took:?} stall_evicts={evicts} ===\"\r\n    );\r\n\r\n    // The viewer kept ticking while the controller conn was wedged.\r\n    assert!(\r\n        ticks_during_wedge > 0,\r\n        \"an attached viewer must keep receiving output while the controller conn is \\\r\n         wedged — the fan-out must never block on the stalled brain conn\"\r\n    );\r\n    // The take COMPLETED and took control (the wedged incumbent was stall-evicted).\r\n    assert!(\r\n        matches!(\r\n            outcome,\r\n            Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)\r\n        ),\r\n        \"a second operator's Control subscribe must COMPLETE by stall-evicting the \\\r\n         wedged incumbent (pre-W2: busy-refused forever). got {outcome:?}\"\r\n    );\r\n    // It resolved WELL under the 10 s watchdog (the stall-evict is prompt, not a hang).\r\n    assert!(\r\n        took < Duration::from_secs(9),\r\n        \"the take must self-heal promptly, not ride the watchdog: {took:?}\"\r\n    );\r\n    // The stall-evict is observable (the broker-side release the daemon-status\r\n    // surface reports).\r\n    assert!(\r\n        evicts >= 1,\r\n        \"the stall-evict must be recorded in the broker-global tally (daemon-status \\\r\n         observability rider); got {evicts}\"\r\n    );\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// UPDATE-WEDGE round-4 — PHYSICAL writer/connection retirement gate.\r\n//\r\n// The companion test above proves LOGICAL isolation: a wedged controller is\r\n// stall-evicted (the ROLE is freed), the viewer keeps ticking, a fresh take\r\n// completes. But hertz's RCA (`docs/UPDATE-WEDGE-ROOT-CAUSE.md` @ f8596ca,\r\n// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\r\n// connection: `stall_evict_controller` drops the `ControllerSink` (one\r\n// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\r\n// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\r\n// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\r\n// wedged inside `write_frame` (WriteFileEx + SleepEx(INFINITE, alertable) — no\r\n// supported write timeout) until the client drops or the process exits (~128 s\r\n// in the field capture). The `handle_conn` reader for that same conn also never\r\n// reaches EOF while the client end stays open. This is LOAD-GATED: it needs a\r\n// real seq>0 controller frame plus a non-draining consumer (seq-0 boot + a\r\n// quiescent update are clean).\r\n//\r\n// This test reproduces that EXACT Windows failure mode with a real PTY child and\r\n// a genuinely black-holed controller connection, then asserts the two things the\r\n// logical gate cannot see:\r\n//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\r\n//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\r\n//     client ever dropping its end;                                    ← RED pre-fix\r\n//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\r\n// Post-fix (conn.rs bounded/cancelable/poison-on-failure framed write): the\r\n// absolute deadline fires out of band, poisons the whole physical conn, aborts\r\n// read+write so `handle_conn` reaches EOF cleanup, and a fresh controller resumes\r\n// from the frozen cursor with NO lost frame.\r\n//\r\n// Every read anywhere in this test rides a bounded thread+channel or a polled\r\n// deadline; the test captures plain values (bools / Options / Instants) as it\r\n// goes and only asserts AFTER teardown, so it can never hang even on broken code\r\n// — worst case it fails an assert after its bounded windows elapse.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\n/// A real PTY child (PowerShell) that emits `SEED`, parks on a first `Read-Host`,\r\n/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\r\n/// and parks again. The seq-0 boot output (VT init + prompt + SEED) gives a real\r\n/// `seq>0` controller frame; the burst floods a non-draining controller's socket\r\n/// so its writer parks INSIDE `write_frame` — the exact black-hole the RCA pinned.\r\n/// Sibling of [`flood_spawn_req`], but FINITE + input-triggered (the flood child\r\n/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\r\n#[cfg(windows)]\r\nfn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\r\n    SpawnReq {\r\n        program: \"powershell\".to_string(),\r\n        args: vec![\r\n            \"-NoProfile\".to_string(),\r\n            \"-Command\".to_string(),\r\n            \"Write-Output SEED; $null = Read-Host; \\\r\n             foreach ($i in 1..1400) { Write-Output ('B' * 1023) }; $null = Read-Host\"\r\n                .to_string(),\r\n        ],\r\n        rows: 24,\r\n        cols: 80,\r\n        endpoint: endpoint.to_string(),\r\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,424,425,426,427,428,429,430,431,432,433,null,439,440]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\brain_decouple.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":834,"totalBytes":19108,"outputLines":433,"outputBytes":19108,"shownRange":{"start":1,"end":433},"nextOffset":434}}},"isError":false,"timestamp":1784195778086}}
{"type":"message","id":"5c9174fe","parentId":"a64c9d22","timestamp":"2026-07-16T09:56:18.088Z","message":{"role":"toolResult","toolCallId":"call_tAlJxHySkoZva8SidnnP5qvz|fc_0b42584a8ab0545d016a58aac206fc81999911dd0f88aa389d","toolName":"grep","content":[{"type":"text","text":"# docs/\n## DEFERRED.md\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## KNOWN-HAZARDS.md\n 387|### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`\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...\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...\n 618|### 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*619|- **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: f...\n 620|- **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 e...\n 621|- **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 ...\n 622|- **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 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 628|- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n*629|- **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 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...\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...\n 642|- **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_drain...\n*643|- **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 644|\n 645|---\n 646|\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n 2|\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...\n 59|or slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\n*60|wedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply \"promoted\"\n 61|while frozen.\n 62|Fix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log\n 63|lock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already\n## PUMP-TRUTH-TRIAGE.md\n 30|\n*31|**3-WAY AGREEMENT GATE (operator ruling 2026-07-08):** PUMP-TRUTH was first RCA'd by deployah on real boxes (foreground `daemon run` trace on enlyzeam) — deployah is the field-evidence authority. So todlando's W1 root-cause finding (WHY the shipped 10s bound doesn't fire) is CROSS-CONFIRMED with deployah before doyle blesses any fix. No fix ships until todlando + deployah + doyle all agree the root against the field evidence. The check sits at the root-confirmed gate (before build begins), so it doesn'...\n 32|\n 33|Co-authored by: doyle\n 34|\n## UPDATE-WEDGE-2-ROUND3-CODEREAD.md\n*1|# 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 2|\n 3|Report-before-fix per RULING 2 §5/§8. doyle asked: pin what the counter-54 reap-drive\n 4|(ce053dc) blocks on each tick, especially whether an UNRESPONSIVE peer lengthens the tick\n## UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\n*1|# UPDATE-WEDGE round-3 RIG VERDICT — net-runtime starvation seam FALSIFIED (todlando, 2026-07-09)\n 2|\n*3|Report-before-fix per `docs/UPDATE-WEDGE-2-ROUND3-RULING.md` §7. Rig lives in the\n 4|`uwedge2` worktree: `crates/spt-daemon/tests/net_worker_starve.rs` + a throwaway\n 5|DIAGNOSTIC probe on `crates/spt-daemon/src/nethost.rs` (`active_dial_tasks()`,\n 6|`net_canary_age_ms()`, `dial_permits_available()` — no REQ, marked throwaway).\n## W2-DESIGN-PROPOSAL.md\n 43|promote only when the OLD generation's subscriber conn is CLOSED or evicted — never\n*44|while blocked writes pend on it. Kills the 22:47 false-promote (`brain.ready` alone\n 45|no longer suffices).\n 46|\n 47|**doc stage** = KNOWN-HAZARDS entry: *\"PTY fan-out and the broker control plane\n## W3-WAVE-GATE-REPORT.md\n 21|while the OLD generation's local controller conn was still wedged → apply \"promoted\" onto\n*22|a frozen control plane (the 22:47 incident). `brain.ready ≠ subscribers drained`.\n 23|\n 24|**Fix — DRAINED precondition on the promotion gate:**\n 25|- `brainproc.rs run_trial`: LATCH `ready_seen` (a ready-then-exit candidate is not demoted;\n...\n 51|  broker-side, and the latched-ready candidate then promotes. Drop the `&& old_gen_drained()`\n*52|  gate → `BRAIN_PROMOTED: candidate v9 ... swap accepted` onto the wedge (the 22:47 repro).\n 53|- Unit (brainproc): `ready_but_old_gen_never_drains_does_not_promote_rolls_back` (RED-first —\n 54|  without the gate it takes the promote path) + `ready_candidate_promotes_once_old_gen_drains`\n 55|  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`\n## W6-GATE-VERDICT.md\n 54|   doc-site pages.\n*55|2. **`since v0.29.0` stamp:** counter-49 = v0.29.0 (standard increment from published v0.28.0 =\n 56|   counter 48). Assumption confirmed against the release ledger. One-word fix only if the cut\n 57|   number differs — it will not.\n 58|\n...\n 66|\n*67|CHANGELOG census covers v0.28.0..HEAD (the counter-49 cut).\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 28|\n*29|**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30|\n 31|## Consequences\n 32|\n...\n 36|- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n*37|- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n 38|- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n 39|\n 40|## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n### 0007-subnet-notification-primitive.md\n 8|\n*9|Three flows independently needed \"deliver something to the user's most-recently-active session and make sure they see it\": the **self-update prompt** (CONTEXT §Self-update), the **consent escalation** (CONTEXT §Consent & security gates — both already described as \"the registry-resolution precursor to PresenceChannel\"), and emerging **subnet events** (node paired, external-subnet pairing request). Each was about to grow its own ad-hoc delivery + \"did the user see it?\" handling. They are the same thing.\n 10|\n 11|## Decision\n 12|\n...\n 16|- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n*17|- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18|- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19|- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n 20|\n### 0010-immutable-home-subnet-fork-not-rehome.md\n 26|\n*27|This sidesteps every re-home hazard: nothing migrates, nothing is stranded, the source's mind and addresses are unchanged until the user explicitly deletes it. The fork's id defaults to the source's bare name if free in the target subnet (the ADR-0006 join-time collision check; else prompt/rename); its adapter follows the normal creation rule.\n 28|\n 29|A fork is deliberately **not an instance** and **not a rename**:\n 30|- **instance** — *same* identity, another node, *synced* mind (ADR-0003).\n### 0016-platform-targeted-update-sets.md\n 44|\n*45|The debug driver should verify convergence after staging. By default, \"expected\" means every reachable node in the selected lab subnet that is pinned to `debug`; maintainer tooling may accept an explicit node list for targeted tests. It watches until every expected debug-pinned reachable node reports the target debug version applied, or returns a timeout table with per-node state such as not pinned, offline, staged awaiting consent, blocked by live broker resources, applied, or rejected.\n 46|\n 47|The debug channel sequence is maintainer-tooling state, not project truth or node identity. It may live in an ignored source-repo local file such as under `target/`; losing it is recoverable by choosing a higher debug version. Deployed nodes only require that the next accepted debug metadata version is greater than the current debug version they have seen.\n 48|\n\n[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":29,"fileCount":13,"files":["docs/DEFERRED.md","docs/KNOWN-HAZARDS.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/PUMP-TRUTH-TRIAGE.md","docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/W2-DESIGN-PROPOSAL.md","docs/W3-WAVE-GATE-REPORT.md","docs/W6-GATE-VERDICT.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0007-subnet-notification-primitive.md","docs/adr/0010-immutable-home-subnet-fork-not-rehome.md","docs/adr/0016-platform-targeted-update-sets.md"],"fileMatches":[{"path":"docs/DEFERRED.md","count":4},{"path":"docs/KNOWN-HAZARDS.md","count":8},{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","count":2},{"path":"docs/PUMP-TRUTH-TRIAGE.md","count":1},{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","count":1},{"path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","count":2},{"path":"docs/W2-DESIGN-PROPOSAL.md","count":1},{"path":"docs/W3-WAVE-GATE-REPORT.md","count":2},{"path":"docs/W6-GATE-VERDICT.md","count":2},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":2},{"path":"docs/adr/0007-subnet-notification-primitive.md","count":2},{"path":"docs/adr/0010-immutable-home-subnet-fork-not-rehome.md","count":1},{"path":"docs/adr/0016-platform-targeted-update-sets.md","count":1}],"truncated":true,"displayContent":"# docs/\n## DEFERRED.md\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## KNOWN-HAZARDS.md\n 387│### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`\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    │...\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    │...\n 618│### 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*619│- **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: f...\n 620│- **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 e...\n 621│- **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 ...\n 622│- **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 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 628│- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n*629│- **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 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    │...\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...\n 642│- **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_drain...\n*643│- **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 644│\n 645│---\n 646│\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n  2│\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   │...\n 59│or slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\n*60│wedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply \"promoted\"\n 61│while frozen.\n 62│Fix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log\n 63│lock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already\n## PUMP-TRUTH-TRIAGE.md\n 30│\n*31│**3-WAY AGREEMENT GATE (operator ruling 2026-07-08):** PUMP-TRUTH was first RCA'd by deployah on real boxes (foreground `daemon run` trace on enlyzeam) — deployah is the field-evidence authority. So todlando's W1 root-cause finding (WHY the shipped 10s bound doesn't fire) is CROSS-CONFIRMED with deployah before doyle blesses any fix. No fix ships until todlando + deployah + doyle all agree the root against the field evidence. The check sits at the root-confirmed gate (before build begins), so it doesn'...\n 32│\n 33│Co-authored by: doyle\n 34│\n## UPDATE-WEDGE-2-ROUND3-CODEREAD.md\n*1│# 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 2│\n 3│Report-before-fix per RULING 2 §5/§8. doyle asked: pin what the counter-54 reap-drive\n 4│(ce053dc) blocks on each tick, especially whether an UNRESPONSIVE peer lengthens the tick\n## UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\n*1│# UPDATE-WEDGE round-3 RIG VERDICT — net-runtime starvation seam FALSIFIED (todlando, 2026-07-09)\n 2│\n*3│Report-before-fix per `docs/UPDATE-WEDGE-2-ROUND3-RULING.md` §7. Rig lives in the\n 4│`uwedge2` worktree: `crates/spt-daemon/tests/net_worker_starve.rs` + a throwaway\n 5│DIAGNOSTIC probe on `crates/spt-daemon/src/nethost.rs` (`active_dial_tasks()`,\n 6│`net_canary_age_ms()`, `dial_permits_available()` — no REQ, marked throwaway).\n## W2-DESIGN-PROPOSAL.md\n 43│promote only when the OLD generation's subscriber conn is CLOSED or evicted — never\n*44│while blocked writes pend on it. Kills the 22:47 false-promote (`brain.ready` alone\n 45│no longer suffices).\n 46│\n 47│**doc stage** = KNOWN-HAZARDS entry: *\"PTY fan-out and the broker control plane\n## W3-WAVE-GATE-REPORT.md\n 21│while the OLD generation's local controller conn was still wedged → apply \"promoted\" onto\n*22│a frozen control plane (the 22:47 incident). `brain.ready ≠ subscribers drained`.\n 23│\n 24│**Fix — DRAINED precondition on the promotion gate:**\n 25│- `brainproc.rs run_trial`: LATCH `ready_seen` (a ready-then-exit candidate is not demoted;\n   │...\n 51│  broker-side, and the latched-ready candidate then promotes. Drop the `&& old_gen_drained()`\n*52│  gate → `BRAIN_PROMOTED: candidate v9 ... swap accepted` onto the wedge (the 22:47 repro).\n 53│- Unit (brainproc): `ready_but_old_gen_never_drains_does_not_promote_rolls_back` (RED-first —\n 54│  without the gate it takes the promote path) + `ready_candidate_promotes_once_old_gen_drains`\n 55│  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`\n## W6-GATE-VERDICT.md\n 54│   doc-site pages.\n*55│2. **`since v0.29.0` stamp:** counter-49 = v0.29.0 (standard increment from published v0.28.0 =\n 56│   counter 48). Assumption confirmed against the release ledger. One-word fix only if the cut\n 57│   number differs — it will not.\n 58│\n   │...\n 66│\n*67│CHANGELOG census covers v0.28.0..HEAD (the counter-49 cut).\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 28│\n*29│**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30│\n 31│## Consequences\n 32│\n   │...\n 36│- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n*37│- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n 38│- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n 39│\n 40│## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n### 0007-subnet-notification-primitive.md\n  8│\n *9│Three flows independently needed \"deliver something to the user's most-recently-active session and make sure they see it\": the **self-update prompt** (CONTEXT §Self-update), the **consent escalation** (CONTEXT §Consent & security gates — both already described as \"the registry-resolution precursor to PresenceChannel\"), and emerging **subnet events** (node paired, external-subnet pairing request). Each was about to grow its own ad-hoc delivery + \"did the user see it?\" handling. They are the same thing.\n 10│\n 11│## Decision\n 12│\n   │...\n 16│- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n*17│- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18│- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19│- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n 20│\n### 0010-immutable-home-subnet-fork-not-rehome.md\n 26│\n*27│This sidesteps every re-home hazard: nothing migrates, nothing is stranded, the source's mind and addresses are unchanged until the user explicitly deletes it. The fork's id defaults to the source's bare name if free in the target subnet (the ADR-0006 join-time collision check; else prompt/rename); its adapter follows the normal creation rule.\n 28│\n 29│A fork is deliberately **not an instance** and **not a rename**:\n 30│- **instance** — *same* identity, another node, *synced* mind (ADR-0003).\n### 0016-platform-targeted-update-sets.md\n 44│\n*45│The debug driver should verify convergence after staging. By default, \"expected\" means every reachable node in the selected lab subnet that is pinned to `debug`; maintainer tooling may accept an explicit node list for targeted tests. It watches until every expected debug-pinned reachable node reports the target debug version applied, or returns a timeout table with per-node state such as not pinned, offline, staged awaiting consent, blocked by live broker resources, applied, or rejected.\n 46│\n 47│The debug channel sequence is maintainer-tooling state, not project truth or node identity. It may live in an ignored source-repo local file such as under `target/`; losing it is recoverable by choosing a higher debug version. Deployed nodes only require that the next accepted debug metadata version is greater than the current debug version they have seen.\n 48│","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195778088}}
{"type":"message","id":"4429bdd0","parentId":"5c9174fe","timestamp":"2026-07-16T09:56:18.123Z","message":{"role":"toolResult","toolCallId":"call_8I27EUjL4h1nOgB68S8YRHxe|fc_0b42584a8ab0545d016a58aac2071c8199862b51d6b6ba5663","toolName":"grep","content":[{"type":"text","text":"# .github/workflows/\n## ci.yml\n 156|        run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"\n*157|      # `cargo nextest` runs every test across ALL test binaries in ONE parallel\n*158|      # pool — `cargo test` runs the binaries SERIALLY (only threads WITHIN a\n 159|      # binary). nextest also builds the test targets, so the old standalone `cargo\n 160|      # build --all-targets` is dropped (clippy --all-targets below still compiles\n 161|      # every target — examples/benches included — so non-test breakage is caught).\n...\n 173|      - name: Test — Phase A (light pool, full parallel)\n*174|        run: cargo nextest run --workspace --no-fail-fast -E 'not ( (package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_spl...\n 175|      - name: Test — Phase B (heavy class, serialized, on a now-quiet box)\n*176|        run: cargo nextest run --workspace --no-fail-fast -E '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|bra...\n*177|      # nextest does not run doctests — keep them on `cargo test --doc`.\n 178|      - name: Doctests\n*179|        run: cargo test --workspace --doc\n 180|      - name: Clippy (deny warnings)\n 181|        run: cargo clippy --workspace --all-targets -- -D warnings\n 182|\n...\n 186|      # source integration. CI clones + builds it, then re-runs the\n*187|      # env-gated E2E leg against the real binary (the normal `cargo test`\n 188|      # sweep silent-skips the same test when SPT_NOTIFY_SHELL_BIN is unset).\n 189|      # The Forkening (ADR-0036) deliberately leaves this on SaberMage: the\n 190|      # adapter repo is public, unforked, and outside the private-fork scope\n...\n 200|        if: runner.os == 'Linux'\n*201|        run: cargo test -p spt --test notify_shell_e2e -- --nocapture\n 202|        env:\n 203|          SPT_NOTIFY_SHELL_BIN: ${{ github.workspace }}/.adapter-notify/target/debug/notify-shell\n 204|      - name: Real-shell E2E (notify) — windows\n 205|        if: runner.os == 'Windows'\n*206|        run: cargo test -p spt --test notify_shell_e2e -- --nocapture\n 207|        env:\n 208|          SPT_NOTIFY_SHELL_BIN: ${{ github.workspace }}\\.adapter-notify\\target\\debug\\notify-shell.exe\n 209|\n...\n 219|      - name: Installer E2E (staged-release rung)\n*220|        run: cargo test -p spt --test oneliner_e2e -- --nocapture\n 221|        env:\n 222|          SPT_INSTALL_E2E: \"1\"\n 223|\n...\n 277|        if: contains(github.event.head_commit.message, '[budget]') || contains(github.event.pull_request.title, '[budget]')\n*278|        run: cargo test -p spt-daemon --test budget --release -- --nocapture\n 279|        env:\n 280|          SPT_BUDGET: \"1\"\n 281|      # Two shells: the Windows runner has no bash on PATH (the first tagged\n...\n 288|            SPT_BUDGET=1 SPT_BUDGET_N=3 SPT_BUDGET_PROGRAM=claude SPT_BUDGET_SETTLE_MS=15000 \\\n*289|              cargo test -p spt-daemon --test budget --release -- --nocapture\n 290|          else\n 291|            echo \"claude not installed on this runner — shell numbers only\"\n 292|          fi\n...\n 301|            $env:SPT_BUDGET_SETTLE_MS = \"15000\"\n*302|            cargo test -p spt-daemon --test budget --release -- --nocapture\n 303|          } else {\n 304|            # ASCII only: Windows PowerShell 5.1 reads the step's temp script\n 305|            # without a BOM as ANSI, so a multibyte dash corrupts the string\n...\n 318|  # latest PUBLISHED split-era release once one exists. The n1_pairing harness is\n*319|  # env-gated (SPT_N1_OLD_BROKER), so the normal `cargo test` sweep silent-skips\n 320|  # it; this job sets the env and turns the skip into a real gate on both runners.\n 321|  n1-gate:\n 322|    # Docs-only PRs skip this job (REQ-CI-DOCS-ONLY-THIN; see `changes`).\n...\n 371|          fi\n*372|          SPT_N1_OLD_BROKER=\"$BIN\" cargo test -p spt --test n1_pairing -- --nocapture\n 373|      - name: N-1 gate (new-brain × old-broker) — windows\n 374|        if: runner.os == 'Windows'\n 375|        # pwsh (PowerShell 7), NOT `powershell` (5.1): the runner's 5.1 execution\n...\n 392|          $env:SPT_N1_OLD_BROKER = $BIN\n*393|          cargo test -p spt --test n1_pairing -- --nocapture\n 394|\n 395|  # ---- D9-6 two-host rig ladder (M4-PLAN §D9b) ----\n 396|  # Opt-in per push: runs ONLY when the head commit message carries\n...\n 427|      - name: Two-host ladder — role B (seed-holder/server)\n*428|        run: cargo test -p spt-daemon --test twohost -- --nocapture\n 429|        env:\n 430|          SPT_TWO_HOST: \"1\"\n 431|          SPT_TWO_HOST_ROLE: \"b\"\n...\n 447|      - name: Two-host gated CLI step — role B (server)\n*448|        run: cargo test -p spt --test twohost_cli -- --nocapture\n 449|        env:\n 450|          SPT_TWO_HOST: \"1\"\n 451|          SPT_TWO_HOST_ROLE: \"b\"\n...\n 470|      - name: Two-host ladder — role A (joiner/driver)\n*471|        run: cargo test -p spt-daemon --test twohost -- --nocapture\n 472|        env:\n 473|          SPT_TWO_HOST: \"1\"\n 474|          SPT_TWO_HOST_ROLE: \"a\"\n...\n 481|      - name: Two-host gated CLI step — role A (driver)\n*482|        run: cargo test -p spt --test twohost_cli -- --nocapture\n 483|        env:\n 484|          SPT_TWO_HOST: \"1\"\n 485|          SPT_TWO_HOST_ROLE: \"a\"\n\n# docs/\n## DORMANCY-BUDGET.md\n 39|(both hosts measure in one tagged push) or a direct local\n*40|`SPT_BUDGET=1 cargo test -p spt-daemon --test budget --release -- --nocapture`.\n 41|\n 42|## Numbers\n 43|\n## FAULT-MATRIX.md\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 32|| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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## FLAKE-LEDGER.md\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## KNOWN-HAZARDS.md\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 264|<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\n...\n 331|- **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*332|- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `app...\n 333|\n 334|### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n 335|- **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...\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 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...\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 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 504|### 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...\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*628|- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 629|- **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 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...\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...\n*642|- **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_drain...\n 643|- **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 644|\n 645|---\n...\n 706|| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job...\n*707|| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\n 708|| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n 709|| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\n 710|| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\n...\n 716|| 7.20 | `spt rc` must forward the SCROLL wheel to the harness — our mouse capture (for right-click paste, 7.18) steals WT's native scroll. Track the harness's mouse-reporting mode from its OUTPUT (DECSET `ESC[?1000/1002/1003h` + `1006h` SGR, and `…l` off; scan survives a split across output chunks) into `MouseMode{enabled,sgr}`; forward `ScrollUp/Down` as an xterm SGR report (`ESC[<64/65;col+1;row+1M`) ONLY when `enabled && sgr`, else drop; Moved/drag/clicks dropped. cfg(windows) only — Unix scrol...\n*717|| 7.21 | Exactly ONE LIVE `controller_writer` per brain↔broker connection + every writer emits an ASCENDING seq stream ⇒ a snap-above consumer over the surviving writer's COMPLETE `[0,end]` replay delivers `[K,end]` with no skip/dup. A brain-restart re-serve double-registered the controller (handoff's `subscribe(from_seq=K)` + the re-serve's `attach_as(sid,0)`) → two writers raced one socket → the consumer's strict reject-gap legacy path saw `got seq 1 want 0` (flaky `attach_survives_target_brain...\n 718|| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-...\n 719|<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->\n 720|| 7.23 | An already-spooled message (WAN-arrived or spooled-while-active) NEVER depends on adapter hook-poll cadence to reach a relay-less spt-hosted endpoint — the daemon drives delivery on the events it owns. TWO daemon triggers feed ONE shared translation-binary inject leg (`spt_daemon::inject::try_spt_hosted_inject`): WAN ingress (`receive_wan` injects before spool — leg 1, `[REQ-WAN-SPT-HOSTED-DELIVERY]`) + the ACTIVE→IDLE edge (`drain_idle_window` claims the pending spool — deferred AND non...\n...\n 731|<!-- [doc->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION] -->\n*732|| 7.38 | Every physical broker-conn write bounded + cancelable + poison-on-failure (independent watchdog aborter; poisoned conn never reused; SendHalf never leaves the conn object) | broker `conn.rs` `BrokerConn`, all write sites (`controller_writer`/`viewer_writer`/`send_frame`/nethost), int `brain_decouple.rs` |\n 733|| 7.31 | The Psyche failure budget counts REAL per-event attempts — every bounded per-event turn feeds a consecutive-N budget (default 3), so per-event churn a resident rate-guard was blind to now counts by construction. Fixes hall-bf's ~12/min re-host churn that ran invisibly (boot records weren't ledger boundaries to the old guard) | `lifecycle.rs` `note_turn_outcome`/`psyche_turn_strikes_exhausted` (stamps `psyche_host_error` on exhaustion, no `status` de-stamp), unit synthetic failure-loop trips th...\n 734|<!-- [doc->REQ-HAZARD-THRASH-GUARD-BLIND] -->\n 735|<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n 10|Gate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,\n*11|nextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change →\n 12|grep tests for OLD behavior as assertions, shared-seam → run ALL seam tests, gate suites\n 13|FOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag — the earmarked\n 14|REQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.\n## TWO-HOST-RUNBOOK.md\n 91|SPT_TWO_HOST_WAIT_SECS=900 \\\n*92|cargo test -p spt-daemon --test twohost -- --nocapture\n 93|```\n 94|\n 95|On HFENDULEAM (PowerShell):\n...\n 100|$env:SPT_TWO_HOST_WAIT_SECS=\"900\"\n*101|cargo test -p spt-daemon --test twohost -- --nocapture\n 102|```\n 103|\n 104|Role A retries the pairing dial until B's ceremony endpoint answers, so exact start\n...\n 289|SPT_TWO_HOST_WAIT_SECS=900 \\\n*290|cargo test -p spt --test twohost_cli -- --nocapture\n 291|```\n 292|\n 293|(and role `a` on the other host; pre-build `cargo build -p mock-adapter\n## UPDATE-WEDGE-2-ROUND3-CODEREAD.md\n 104|The mechanism is a code-proven deadlock, so the rig CONFIRMS + GUARDS rather than discovers.\n*105|brain+broker+PTY (`brain_decouple` template): N sessions actively producing output, the brain\n 106|resume-subscribed onto its request/reply conn; RED-first — the heartbeat stalls and an\n 107|active-streaming controller is stall-evicted >15s WITHOUT a dead peer needing to be present\n 108|(proving dead-peer-independence). Assert BOTH severities (respawn interleave + steady-state\n## UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\n 78|\n*79|Next rig (needs doyle's authorization — brain+broker+PTY stack, `brain_decouple`\n 80|template + a real pump round with a dead peer): assert whether (1) the counter-54\n 81|reap-drive and/or (2) startup sequencing blocks the brain from draining an\n 82|actively-streaming controller >15s. That is the real decider now. The pure-NetHost\n...\n 86|\n*87|`cargo nextest run -p spt-daemon --test net_worker_starve` (or `--test-threads=1`).\n 88|Hermetic (relays disabled, no discovery, loopback QUIC); no SPT_HOME/PTY needed.\n## W2-BUILT-REPORT.md\n 53|## Int rig (gate condition #2)\n*54|`tests/brain_decouple.rs` — the faithful in-process analog: a REMOTE controller R takes\n 55|control then stops reading, so the broker's R-writer parks in `write_frame` exactly as\n 56|against a suspended brain (peer stopped draining). Asserts, with R wedged past the shrunk\n 57|deadline: viewer keeps ticking, a second operator's `Control` subscribe COMPLETES (stall-\n...\n 67|- **nextest `--workspace`: 1668/1668** (5 leaky, 1 skipped) — the 5 new W2 tests included.\n*68|- seam sweep (gate condition #3): `brain_decouple` 1/1, `inject_control_wedge` 22/22,\n 69|  `broker` units 30/30 (incl the 4 W2 units), `driven_by_selfheal` + `control_stamp_lifetime` green.\n 70|- `traceable-reqs check` exit 0 — REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE `+doc +impl +unit +int`.\n 71|- per-wave activation (condition #4): REQ activated `[doc,impl,unit,int]` in the work-start\n## W2-GATE-VERDICT.md\n 12|  (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE `+doc +impl +unit +int`).\n*13|- Linux leg (kitsubito, /tmp/w2-gate @fbf8ab7): `brain_decouple` +\n 14|  `inject_control_wedge` + `driven_by_selfheal` 19/19; spt-daemon lib 451/451.\n 15|\n 16|FLAKE LEDGER (gate, 2026-07-07 — both load-class on the shared box, live daemon +\n...\n 65|  NtSuspendProcess rig with cursor-replay byte-identity as the int evidence.\n*66|- `tests/brain_decouple.rs` header line \"the newcomer receives replayed output\n 67|  from its cursor (resume loses nothing)\" — not asserted.\n 68|\n 69|Amend both to state what IS proven (analog + four asserts; replay integrity\n## W5-WAVE-GATE-REPORT.md\n 95|- `xtask check`: **OK** (docs-drift + token; no CLI surface changed this wave).\n*96|- `cargo nextest run --workspace --no-fail-fast`: **1705 passed / 1706** (8 leaky, 1 skip).\n 97|  - The one red — `spt-store registry::tests::concurrent_registration_never_locks` — is the\n 98|    known AV-hold concurrency flake (W2 ledger): **PASSES isolated** (`1/1`), a contention\n 99|    flake under full parallel load, not a W5 regression.\n\n# crates/spt-daemon/\n\n## src/\n### broker.rs\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 2793|    /// drains through the REAL logical paths (a take's `stall_evict_controller`\n 2794|    /// or a `KIND_SESSIONS` reap), exactly like a production stall-evict.\n 2795|    /// Returns false if the session doesn't exist or the seed can't be built.\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 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 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_decouple.rs\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### brain_resume_conn_deadlock.rs\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 33|use std::sync::atomic::{AtomicU32, Ordering};\n### brain_swap.rs\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### 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...\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*75|//! bare `cargo test --test conn_blackhole_lifecycle` on Windows. Every read in\n 76|//! this test rides a bounded thread+channel or a polled deadline; the test\n 77|//! captures plain values as it goes and only asserts AFTER teardown, so it can\n 78|//! never hang even on broken code.\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### control_stamp_lifetime.rs\n 37|//! a REAL broker per test; run isolated (the driven_by_selfheal precedent):\n*38|//!   - `cargo nextest run --test control_stamp_lifetime`  (CI's path), or\n*39|//!   - `cargo test --test control_stamp_lifetime -- --test-threads=1`.\n 40|//!\n*41|//! Do NOT run a bare `cargo test --test control_stamp_lifetime` on Windows.\n 42|\n 43|use std::collections::BTreeSet;\n 44|use std::sync::atomic::{AtomicU32, Ordering};\n### daemon_e2e.rs\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\nShowing files 1-20 of 44. Use skip=20 for the next page, or narrow paths/pattern.\n\nSkipped missing paths: justfile\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".github, docs, Makefile*, Cargo.toml, crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":67,"fileCount":20,"files":[".github/workflows/ci.yml","docs/DORMANCY-BUDGET.md","docs/FAULT-MATRIX.md","docs/FLAKE-LEDGER.md","docs/KNOWN-HAZARDS.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/TWO-HOST-RUNBOOK.md","docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/W2-BUILT-REPORT.md","docs/W2-GATE-VERDICT.md","docs/W5-WAVE-GATE-REPORT.md","crates/spt-daemon/src/broker.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/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/daemon_e2e.rs"],"fileMatches":[{"path":".github/workflows/ci.yml","count":20},{"path":"docs/DORMANCY-BUDGET.md","count":1},{"path":"docs/FAULT-MATRIX.md","count":1},{"path":"docs/FLAKE-LEDGER.md","count":2},{"path":"docs/KNOWN-HAZARDS.md","count":10},{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","count":1},{"path":"docs/TWO-HOST-RUNBOOK.md","count":3},{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","count":1},{"path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","count":2},{"path":"docs/W2-BUILT-REPORT.md","count":2},{"path":"docs/W2-GATE-VERDICT.md","count":2},{"path":"docs/W5-WAVE-GATE-REPORT.md","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":1},{"path":"crates/spt-daemon/tests/attach.rs","count":3},{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":3},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":3},{"path":"crates/spt-daemon/tests/brain_swap.rs","count":1},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":6},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":3},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":1}],"truncated":true,"fileLimitReached":20,"displayContent":"# .github/workflows/\n## ci.yml\n 156│        run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"\n*157│      # `cargo nextest` runs every test across ALL test binaries in ONE parallel\n*158│      # pool — `cargo test` runs the binaries SERIALLY (only threads WITHIN a\n 159│      # binary). nextest also builds the test targets, so the old standalone `cargo\n 160│      # build --all-targets` is dropped (clippy --all-targets below still compiles\n 161│      # every target — examples/benches included — so non-test breakage is caught).\n    │...\n 173│      - name: Test — Phase A (light pool, full parallel)\n*174│        run: cargo nextest run --workspace --no-fail-fast -E 'not ( (package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_spl...\n 175│      - name: Test — Phase B (heavy class, serialized, on a now-quiet box)\n*176│        run: cargo nextest run --workspace --no-fail-fast -E '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|bra...\n*177│      # nextest does not run doctests — keep them on `cargo test --doc`.\n 178│      - name: Doctests\n*179│        run: cargo test --workspace --doc\n 180│      - name: Clippy (deny warnings)\n 181│        run: cargo clippy --workspace --all-targets -- -D warnings\n 182│\n    │...\n 186│      # source integration. CI clones + builds it, then re-runs the\n*187│      # env-gated E2E leg against the real binary (the normal `cargo test`\n 188│      # sweep silent-skips the same test when SPT_NOTIFY_SHELL_BIN is unset).\n 189│      # The Forkening (ADR-0036) deliberately leaves this on SaberMage: the\n 190│      # adapter repo is public, unforked, and outside the private-fork scope\n    │...\n 200│        if: runner.os == 'Linux'\n*201│        run: cargo test -p spt --test notify_shell_e2e -- --nocapture\n 202│        env:\n 203│          SPT_NOTIFY_SHELL_BIN: ${{ github.workspace }}/.adapter-notify/target/debug/notify-shell\n 204│      - name: Real-shell E2E (notify) — windows\n 205│        if: runner.os == 'Windows'\n*206│        run: cargo test -p spt --test notify_shell_e2e -- --nocapture\n 207│        env:\n 208│          SPT_NOTIFY_SHELL_BIN: ${{ github.workspace }}\\.adapter-notify\\target\\debug\\notify-shell.exe\n 209│\n    │...\n 219│      - name: Installer E2E (staged-release rung)\n*220│        run: cargo test -p spt --test oneliner_e2e -- --nocapture\n 221│        env:\n 222│          SPT_INSTALL_E2E: \"1\"\n 223│\n    │...\n 277│        if: contains(github.event.head_commit.message, '[budget]') || contains(github.event.pull_request.title, '[budget]')\n*278│        run: cargo test -p spt-daemon --test budget --release -- --nocapture\n 279│        env:\n 280│          SPT_BUDGET: \"1\"\n 281│      # Two shells: the Windows runner has no bash on PATH (the first tagged\n    │...\n 288│            SPT_BUDGET=1 SPT_BUDGET_N=3 SPT_BUDGET_PROGRAM=claude SPT_BUDGET_SETTLE_MS=15000 \\\n*289│              cargo test -p spt-daemon --test budget --release -- --nocapture\n 290│          else\n 291│            echo \"claude not installed on this runner — shell numbers only\"\n 292│          fi\n    │...\n 301│            $env:SPT_BUDGET_SETTLE_MS = \"15000\"\n*302│            cargo test -p spt-daemon --test budget --release -- --nocapture\n 303│          } else {\n 304│            # ASCII only: Windows PowerShell 5.1 reads the step's temp script\n 305│            # without a BOM as ANSI, so a multibyte dash corrupts the string\n    │...\n 318│  # latest PUBLISHED split-era release once one exists. The n1_pairing harness is\n*319│  # env-gated (SPT_N1_OLD_BROKER), so the normal `cargo test` sweep silent-skips\n 320│  # it; this job sets the env and turns the skip into a real gate on both runners.\n 321│  n1-gate:\n 322│    # Docs-only PRs skip this job (REQ-CI-DOCS-ONLY-THIN; see `changes`).\n    │...\n 371│          fi\n*372│          SPT_N1_OLD_BROKER=\"$BIN\" cargo test -p spt --test n1_pairing -- --nocapture\n 373│      - name: N-1 gate (new-brain × old-broker) — windows\n 374│        if: runner.os == 'Windows'\n 375│        # pwsh (PowerShell 7), NOT `powershell` (5.1): the runner's 5.1 execution\n    │...\n 392│          $env:SPT_N1_OLD_BROKER = $BIN\n*393│          cargo test -p spt --test n1_pairing -- --nocapture\n 394│\n 395│  # ---- D9-6 two-host rig ladder (M4-PLAN §D9b) ----\n 396│  # Opt-in per push: runs ONLY when the head commit message carries\n    │...\n 427│      - name: Two-host ladder — role B (seed-holder/server)\n*428│        run: cargo test -p spt-daemon --test twohost -- --nocapture\n 429│        env:\n 430│          SPT_TWO_HOST: \"1\"\n 431│          SPT_TWO_HOST_ROLE: \"b\"\n    │...\n 447│      - name: Two-host gated CLI step — role B (server)\n*448│        run: cargo test -p spt --test twohost_cli -- --nocapture\n 449│        env:\n 450│          SPT_TWO_HOST: \"1\"\n 451│          SPT_TWO_HOST_ROLE: \"b\"\n    │...\n 470│      - name: Two-host ladder — role A (joiner/driver)\n*471│        run: cargo test -p spt-daemon --test twohost -- --nocapture\n 472│        env:\n 473│          SPT_TWO_HOST: \"1\"\n 474│          SPT_TWO_HOST_ROLE: \"a\"\n    │...\n 481│      - name: Two-host gated CLI step — role A (driver)\n*482│        run: cargo test -p spt --test twohost_cli -- --nocapture\n 483│        env:\n 484│          SPT_TWO_HOST: \"1\"\n 485│          SPT_TWO_HOST_ROLE: \"a\"\n\n# docs/\n## DORMANCY-BUDGET.md\n 39│(both hosts measure in one tagged push) or a direct local\n*40│`SPT_BUDGET=1 cargo test -p spt-daemon --test budget --release -- --nocapture`.\n 41│\n 42│## Numbers\n 43│\n## FAULT-MATRIX.md\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 32│| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\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## FLAKE-LEDGER.md\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## KNOWN-HAZARDS.md\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 264│<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\n    │...\n 331│- **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*332│- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `app...\n 333│\n 334│### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n 335│- **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    │...\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 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    │...\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 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 504│### 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    │...\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*628│- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 629│- **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 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    │...\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...\n*642│- **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_drain...\n 643│- **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 644│\n 645│---\n    │...\n 706│| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job...\n*707│| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\n 708│| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n 709│| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\n 710│| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\n    │...\n 716│| 7.20 | `spt rc` must forward the SCROLL wheel to the harness — our mouse capture (for right-click paste, 7.18) steals WT's native scroll. Track the harness's mouse-reporting mode from its OUTPUT (DECSET `ESC[?1000/1002/1003h` + `1006h` SGR, and `…l` off; scan survives a split across output chunks) into `MouseMode{enabled,sgr}`; forward `ScrollUp/Down` as an xterm SGR report (`ESC[<64/65;col+1;row+1M`) ONLY when `enabled && sgr`, else drop; Moved/drag/clicks dropped. cfg(windows) only — Unix scrol...\n*717│| 7.21 | Exactly ONE LIVE `controller_writer` per brain↔broker connection + every writer emits an ASCENDING seq stream ⇒ a snap-above consumer over the surviving writer's COMPLETE `[0,end]` replay delivers `[K,end]` with no skip/dup. A brain-restart re-serve double-registered the controller (handoff's `subscribe(from_seq=K)` + the re-serve's `attach_as(sid,0)`) → two writers raced one socket → the consumer's strict reject-gap legacy path saw `got seq 1 want 0` (flaky `attach_survives_target_brain...\n 718│| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-...\n 719│<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->\n 720│| 7.23 | An already-spooled message (WAN-arrived or spooled-while-active) NEVER depends on adapter hook-poll cadence to reach a relay-less spt-hosted endpoint — the daemon drives delivery on the events it owns. TWO daemon triggers feed ONE shared translation-binary inject leg (`spt_daemon::inject::try_spt_hosted_inject`): WAN ingress (`receive_wan` injects before spool — leg 1, `[REQ-WAN-SPT-HOSTED-DELIVERY]`) + the ACTIVE→IDLE edge (`drain_idle_window` claims the pending spool — deferred AND non...\n    │...\n 731│<!-- [doc->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION] -->\n*732│| 7.38 | Every physical broker-conn write bounded + cancelable + poison-on-failure (independent watchdog aborter; poisoned conn never reused; SendHalf never leaves the conn object) | broker `conn.rs` `BrokerConn`, all write sites (`controller_writer`/`viewer_writer`/`send_frame`/nethost), int `brain_decouple.rs` |\n 733│| 7.31 | The Psyche failure budget counts REAL per-event attempts — every bounded per-event turn feeds a consecutive-N budget (default 3), so per-event churn a resident rate-guard was blind to now counts by construction. Fixes hall-bf's ~12/min re-host churn that ran invisibly (boot records weren't ledger boundaries to the old guard) | `lifecycle.rs` `note_turn_outcome`/`psyche_turn_strikes_exhausted` (stamps `psyche_host_error` on exhaustion, no `status` de-stamp), unit synthetic failure-loop trips th...\n 734│<!-- [doc->REQ-HAZARD-THRASH-GUARD-BLIND] -->\n 735│<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n 10│Gate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,\n*11│nextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change →\n 12│grep tests for OLD behavior as assertions, shared-seam → run ALL seam tests, gate suites\n 13│FOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag — the earmarked\n 14│REQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.\n## TWO-HOST-RUNBOOK.md\n  91│SPT_TWO_HOST_WAIT_SECS=900 \\\n *92│cargo test -p spt-daemon --test twohost -- --nocapture\n  93│```\n  94│\n  95│On HFENDULEAM (PowerShell):\n    │...\n 100│$env:SPT_TWO_HOST_WAIT_SECS=\"900\"\n*101│cargo test -p spt-daemon --test twohost -- --nocapture\n 102│```\n 103│\n 104│Role A retries the pairing dial until B's ceremony endpoint answers, so exact start\n    │...\n 289│SPT_TWO_HOST_WAIT_SECS=900 \\\n*290│cargo test -p spt --test twohost_cli -- --nocapture\n 291│```\n 292│\n 293│(and role `a` on the other host; pre-build `cargo build -p mock-adapter\n## UPDATE-WEDGE-2-ROUND3-CODEREAD.md\n 104│The mechanism is a code-proven deadlock, so the rig CONFIRMS + GUARDS rather than discovers.\n*105│brain+broker+PTY (`brain_decouple` template): N sessions actively producing output, the brain\n 106│resume-subscribed onto its request/reply conn; RED-first — the heartbeat stalls and an\n 107│active-streaming controller is stall-evicted >15s WITHOUT a dead peer needing to be present\n 108│(proving dead-peer-independence). Assert BOTH severities (respawn interleave + steady-state\n## UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\n 78│\n*79│Next rig (needs doyle's authorization — brain+broker+PTY stack, `brain_decouple`\n 80│template + a real pump round with a dead peer): assert whether (1) the counter-54\n 81│reap-drive and/or (2) startup sequencing blocks the brain from draining an\n 82│actively-streaming controller >15s. That is the real decider now. The pure-NetHost\n   │...\n 86│\n*87│`cargo nextest run -p spt-daemon --test net_worker_starve` (or `--test-threads=1`).\n 88│Hermetic (relays disabled, no discovery, loopback QUIC); no SPT_HOME/PTY needed.\n## W2-BUILT-REPORT.md\n 53│## Int rig (gate condition #2)\n*54│`tests/brain_decouple.rs` — the faithful in-process analog: a REMOTE controller R takes\n 55│control then stops reading, so the broker's R-writer parks in `write_frame` exactly as\n 56│against a suspended brain (peer stopped draining). Asserts, with R wedged past the shrunk\n 57│deadline: viewer keeps ticking, a second operator's `Control` subscribe COMPLETES (stall-\n   │...\n 67│- **nextest `--workspace`: 1668/1668** (5 leaky, 1 skipped) — the 5 new W2 tests included.\n*68│- seam sweep (gate condition #3): `brain_decouple` 1/1, `inject_control_wedge` 22/22,\n 69│  `broker` units 30/30 (incl the 4 W2 units), `driven_by_selfheal` + `control_stamp_lifetime` green.\n 70│- `traceable-reqs check` exit 0 — REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE `+doc +impl +unit +int`.\n 71│- per-wave activation (condition #4): REQ activated `[doc,impl,unit,int]` in the work-start\n## W2-GATE-VERDICT.md\n 12│  (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE `+doc +impl +unit +int`).\n*13│- Linux leg (kitsubito, /tmp/w2-gate @fbf8ab7): `brain_decouple` +\n 14│  `inject_control_wedge` + `driven_by_selfheal` 19/19; spt-daemon lib 451/451.\n 15│\n 16│FLAKE LEDGER (gate, 2026-07-07 — both load-class on the shared box, live daemon +\n   │...\n 65│  NtSuspendProcess rig with cursor-replay byte-identity as the int evidence.\n*66│- `tests/brain_decouple.rs` header line \"the newcomer receives replayed output\n 67│  from its cursor (resume loses nothing)\" — not asserted.\n 68│\n 69│Amend both to state what IS proven (analog + four asserts; replay integrity\n## W5-WAVE-GATE-REPORT.md\n 95│- `xtask check`: **OK** (docs-drift + token; no CLI surface changed this wave).\n*96│- `cargo nextest run --workspace --no-fail-fast`: **1705 passed / 1706** (8 leaky, 1 skip).\n 97│  - The one red — `spt-store registry::tests::concurrent_registration_never_locks` — is the\n 98│    known AV-hold concurrency flake (W2 ledger): **PASSES isolated** (`1/1`), a contention\n 99│    flake under full parallel load, not a W5 regression.\n\n# crates/spt-daemon/\n\n## src/\n### broker.rs\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 2793│    /// drains through the REAL logical paths (a take's `stall_evict_controller`\n 2794│    /// or a `KIND_SESSIONS` reap), exactly like a production stall-evict.\n 2795│    /// Returns false if the session doesn't exist or the seed can't be built.\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 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 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_decouple.rs\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### brain_resume_conn_deadlock.rs\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 33│use std::sync::atomic::{AtomicU32, Ordering};\n### brain_swap.rs\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### 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    │...\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 *75│//! bare `cargo test --test conn_blackhole_lifecycle` on Windows. Every read in\n  76│//! this test rides a bounded thread+channel or a polled deadline; the test\n  77│//! captures plain values as it goes and only asserts AFTER teardown, so it can\n  78│//! never hang even on broken code.\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### control_stamp_lifetime.rs\n 37│//! a REAL broker per test; run isolated (the driven_by_selfheal precedent):\n*38│//!   - `cargo nextest run --test control_stamp_lifetime`  (CI's path), or\n*39│//!   - `cargo test --test control_stamp_lifetime -- --test-threads=1`.\n 40│//!\n*41│//! Do NOT run a bare `cargo test --test control_stamp_lifetime` on Windows.\n 42│\n 43│use std::collections::BTreeSet;\n 44│use std::sync::atomic::{AtomicU32, Ordering};\n### daemon_e2e.rs\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.","missingPaths":["justfile"],"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195778123}}
{"type":"message","id":"124953ed","parentId":"4429bdd0","timestamp":"2026-07-16T09:56:31.761Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning daemon refresh test harness**\n\n**Designing multi-session bidirectional test**"},{"type":"toolCall","id":"call_5gktamwYR0zG4F9X9BFfhwtW|fc_0b42584a8ab0545d016a58aacfb8b481999cd569b94c4287fb","name":"read","arguments":{"i":"Reading update incident triage","path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","selector":"1-110"}},{"type":"toolCall","id":"call_8GIpIV0zkl3HHhdueAI3PXWQ|fc_0b42584a8ab0545d016a58aacfb8c481998d4479f7a66db7a2","name":"read","arguments":{"i":"Reading deadlock code analysis","path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","selector":"1-160"}},{"type":"toolCall","id":"call_yXvpOgsZrIfb6iARaoT7DKOx|fc_0b42584a8ab0545d016a58aacfb8c881999ebf2606727c991c","name":"read","arguments":{"i":"Reading incident rig verdict","path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","selector":"1-130"}},{"type":"toolCall","id":"call_4QkvnGmHOyuos7jlWIaZK49a|fc_0b42584a8ab0545d016a58aacfb8d08199865be776c32d8299","name":"read","arguments":{"i":"Reading promotion gate report","path":"docs/W3-WAVE-GATE-REPORT.md","selector":"1-100"}},{"type":"toolCall","id":"call_00UQSFeCXSEfa2s6MvHZVdhY|fc_0b42584a8ab0545d016a58aacfb8d481998a63551b6f146f76","name":"grep","arguments":{"i":"Finding dummy PTY behavior","pattern":"DUMMY_HARNESS_TICK|heartbeat-ms|mode.*dummy|stdin|read_line|read_to|echo","path":"crates/mock-adapter;crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_UiIBCViJs6REnjprUOAhXRZz|fc_0b42584a8ab0545d016a58aacfb8dc8199824f46a8e1302074","name":"grep","arguments":{"i":"Finding refresh command test hooks","pattern":"refresh_brain|request_brain_restart|BrainRestart|cmd_daemon_refresh|daemon.*refresh","path":"crates/spt-daemon;crates/spt","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_F176zZdk76ax6zGkzMRQdCkH|fc_0b42584a8ab0545d016a58aacfb8e881998186d2592e75bef9","name":"read","arguments":{"i":"Reading CI heavy command fully","path":".github/workflows/ci.yml","selector":"150-180"}},{"type":"toolCall","id":"call_im8YAVOrWYaYmYTFo1OsQ7Gi|fc_0b42584a8ab0545d016a58aacfb8ec81999aa9a5a256acf779","name":"read","arguments":{"i":"Reading fault matrix restart rows","path":"docs/FAULT-MATRIX.md","selector":"1-65"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":33021,"output":644,"cacheRead":110080,"cacheWrite":0,"totalTokens":143745,"reasoningTokens":252,"cost":{"input":0.165105,"output":0.01932,"cacheRead":0.05504,"cacheWrite":0,"total":0.239465}},"stopReason":"toolUse","timestamp":1784195778170,"responseId":"resp_0b42584a8ab0545d016a58aac2810481998bdeb73bc78a6338","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aac418508199b7acacc08a2e274e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrPnp70fI8EyaDwazZJlwACt9rFsFmtEPXk7_Op7ZyVLqkVyqjhkMijxefDcGt3U2658tu89BI29MiOVGxm33-FprrL6YLgispxvZ6SWYAkjQ37SCCrsaVjSqBGdP-5rxoKpMGzOcSJBXUYD1AvdW1Veh7mN1oGiEqNEstGq9weg5nrOGRCc1cpv2K7usiYog6hTZsqoJmOKMXWNmMflgUtujI3r5Th6fuwR3yRDq5Ft-NFnOcdti9l0XIbcBAYf2MHLsCOnWWWM3E6_tO6ob9Exv99mKHhJThuUWvmH7voFel1R-DXm0e8QbMeEjLXnWtALvjNUDq0U5V4FgRS3cHxq2F0Ta3uymBro0CyXx2hDhCc5HhEiVjyLXYQydJDx5hurMhjW_k8EEq7G1BLPdLev6bUwY1Z78CHzkCV3qNVXtMzKdiuzIDHn47JEt4ywsuvi8XWedYygnCg9r02nfPyFyDacwvCuwg250jLdpJr1RWMfhgQkiz_sNftVyJGsEiem7AiCI7NXm7bhjFEhj8XVARTVwlF-iChYP3qX4HwWv7U2OWGp-bwBoBlEVVnG8dqK8m6DianuFLNtEw2gfQMlecPuaKWkhR_6K4gqKcr6xT2TQCiFLvRycU3kHfkQM1bxHTRHllgqFfxdaEqXyjnDcT0Uc8oIT925I0aKhbQDIpTd77BNsLt2u64F3qJJc4EFMDqCAOCxlov24U3-IPKC0Lg5OICt30wpIHK3dEWKtCX_DVCOeIQFXpgRQqJ_M7-LNicAdVv0QzTXklUIf1vJUYtzMLastCIb_G8dSMnvQKYg8rMDmb2mEAwfTBXlk1rtg3PaAAiqY4legl6vM3tdJD69586Iuc35hrgvwN7FzYJgCJJrXnLMopkB1lB_7WfYmEJznFXxJjbfzRX9WwuaY5mostCSMMb2UxApsLHS640OKJg-gCAEwQsUuBaI01XMTkuqLdV1-7NwfvwY_kulUMXxF6BqOHJ6JN5782t_CHzCqur8xeiBf2965tm2i6yFdp76B8MYf4Y0K5XkaJ90jAxh2sjJ41gzbtclcikn1Kv_JxxQRJBJd6gDnwMx6WBcYIeX4cQr0qzrSZMbfFRJMiaX41iHGkqKJzDLgzCALRS39CruY1kR6udMThgO0P8zIlbtWfTwIBXY_QUGr5s4oV0_c836lUCUmBQ0yLq21CP6lQdSNzQKheE_qKf9iSxtpM__8X6dnYlWxUuE3ECULZfRi_bvYoGQW_xHFST5lxnF4J2vQLWngHNQ1TQ4U29IQd7Z8mOf5dIFmX3keATyTuhc3RR6z-zq5mdBPKFmYcPwF-xPHmYhqxNp86_q-uDbycmXjlLCz22t8IptbPAAIJf1hhENSJj3O85WE_3a7kQyR18I69EB4IbPLeHeNBxXF56KZH_FRmGq3tg95swJq9q9ONVMlWWKA8YfIT46e9rdHvUFfmmjQO_I9woS0NGPITsOYUFcvBQzhbCHfvFmItiNVZSLrFOeCOjweUHazG__wIjZ0kqbUjy70CZANQbhvvfbKDCf0Jzpu_nS6pIuG0K1AWHg4B_q-FovFOIi2lgHdjlcxUZcYussPUmDNNWT9wkpg_VE5IGWhoJUPQqOybbYU3S6pm4DJFgbCWYv04F-G_E_GNZVeT6rxLYcJJ1rFcR8ZP0IvRjFIoGOgQ0QVA2Ir0l48H4BD_3_CRnKS8JE1AcXl7tUYf0ISS6f0PppB0A6-CWlFUk49aqpNM3C015oEEDDlfjsCNOdedKZC7MPS6W-nw8GPzBUzSZSESsh0EuV1K0hzx_ilKMr4UdwJ7-j0Ol3o-VXDJSnznOiYMokeMZnpItNyCWstmZdWHRe1LV0Y9xsGgPxtzG7ie3pB8Rtvwpov0WmFfCHG7rPfs8sBTLq6HOuCCNPiNK7SwIG_a8GZ5tAdRcEE_Ksy6aYAvlHbHxfFKUiFyqEKKzJAExm00HqGSgbC5TO0JVHzs533-5AyMf21GLrGZyDH9yeGF1AH7j5RHl9pmXuUfbfu8BGgIADb890LYR8BLueXaiL7UO7ROYWhu3fysQsVvfuFPeKy2zgVo0WN_sOjbAKeXxyttX_8R65fTQLLZxCsRispAXxZVuktEBLL77HN_ojej2YPRJB-iLdQCpZHPZDLEx_Kk1F9UDSxJjdM-gzBrj3PsGdFTJBD8kiTReavZSd2DqABMgZ9EQkdAWAyxaZVDxzgDpY8FlECi1QhzBmN_OBfuIpfQI6yUPY8-ee8RGvYzLIWKVs-cXXqgOwkoxksLAcC0kM7XM-3s1Cyy2ktRgzqY7ZclnysDFGsM1aREq38bd9rfyxtNipv5N_yVLKlVWmLT3NudDWobauJ357puUzDykZuIzMt-ZW9PnEnRewN7z5osKGYfCLBSLRyAbAdIzRoWPLvQePsuddLHmb6HXnXudJdYvAef3NKdYWosfKOPTVvVMFAkkuyCbEYpLmD3Expb-cMm7n4EnbGPkudqK5SHIKG3S1kXcVMY-7SuNgS7i3EQuDT1QbXP7tR1HthU=","summary":[{"type":"summary_text","text":"**Planning daemon refresh test harness**"},{"type":"summary_text","text":"**Designing multi-session bidirectional test**"}]},{"id":"fc_0b42584a8ab0545d016a58aacfb8b481999cd569b94c4287fb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update incident triage\",\"path\":\"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\",\"selector\":\"1-110\"}","call_id":"call_5gktamwYR0zG4F9X9BFfhwtW","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aacfb8c481998d4479f7a66db7a2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading deadlock code analysis\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\",\"selector\":\"1-160\"}","call_id":"call_8GIpIV0zkl3HHhdueAI3PXWQ","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aacfb8c881999ebf2606727c991c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading incident rig verdict\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\",\"selector\":\"1-130\"}","call_id":"call_yXvpOgsZrIfb6iARaoT7DKOx","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aacfb8d08199865be776c32d8299","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading promotion gate report\",\"path\":\"docs/W3-WAVE-GATE-REPORT.md\",\"selector\":\"1-100\"}","call_id":"call_4QkvnGmHOyuos7jlWIaZK49a","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aacfb8d481998a63551b6f146f76","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dummy PTY behavior\",\"pattern\":\"DUMMY_HARNESS_TICK|heartbeat-ms|mode.*dummy|stdin|read_line|read_to|echo\",\"path\":\"crates/mock-adapter;crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_00UQSFeCXSEfa2s6MvHZVdhY","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aacfb8dc8199824f46a8e1302074","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding refresh command test hooks\",\"pattern\":\"refresh_brain|request_brain_restart|BrainRestart|cmd_daemon_refresh|daemon.*refresh\",\"path\":\"crates/spt-daemon;crates/spt\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_UiIBCViJs6REnjprUOAhXRZz","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aacfb8e881998186d2592e75bef9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading CI heavy command fully\",\"path\":\".github/workflows/ci.yml\",\"selector\":\"150-180\"}","call_id":"call_F176zZdk76ax6zGkzMRQdCkH","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aacfb8ec81999aa9a5a256acf779","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading fault matrix restart rows\",\"path\":\"docs/FAULT-MATRIX.md\",\"selector\":\"1-65\"}","call_id":"call_im8YAVOrWYaYmYTFo1OsQ7Gi","name":"read"}]},"duration":13590.203300000168,"ttft":1902.8338999999687,"contextSnapshot":{"promptTokens":143101,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5gktamwYR0zG4F9X9BFfhwtW|fc_0b42584a8ab0545d016a58aacfb8b481999cd569b94c4287fb","toolName":"read","startedAt":"2026-07-16T09:56:31.761Z","args":{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md"},"intent":"Reading update incident triage"},"id":"249d9703","parentId":"124953ed","timestamp":"2026-07-16T09:56:31.761Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8GIpIV0zkl3HHhdueAI3PXWQ|fc_0b42584a8ab0545d016a58aacfb8c481998d4479f7a66db7a2","toolName":"read","startedAt":"2026-07-16T09:56:31.761Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md"},"intent":"Reading deadlock code analysis"},"id":"104e4cc8","parentId":"249d9703","timestamp":"2026-07-16T09:56:31.761Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yXvpOgsZrIfb6iARaoT7DKOx|fc_0b42584a8ab0545d016a58aacfb8c881999ebf2606727c991c","toolName":"read","startedAt":"2026-07-16T09:56:31.762Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md"},"intent":"Reading incident rig verdict"},"id":"06ece2ba","parentId":"104e4cc8","timestamp":"2026-07-16T09:56:31.762Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4QkvnGmHOyuos7jlWIaZK49a|fc_0b42584a8ab0545d016a58aacfb8d08199865be776c32d8299","toolName":"read","startedAt":"2026-07-16T09:56:31.762Z","args":{"path":"docs/W3-WAVE-GATE-REPORT.md"},"intent":"Reading promotion gate report"},"id":"82d9ed8a","parentId":"06ece2ba","timestamp":"2026-07-16T09:56:31.762Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_00UQSFeCXSEfa2s6MvHZVdhY|fc_0b42584a8ab0545d016a58aacfb8d481998a63551b6f146f76","toolName":"grep","startedAt":"2026-07-16T09:56:31.762Z","args":{"path":"crates/mock-adapter;crates"},"intent":"Finding dummy PTY behavior"},"id":"f1300f74","parentId":"82d9ed8a","timestamp":"2026-07-16T09:56:31.762Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UiIBCViJs6REnjprUOAhXRZz|fc_0b42584a8ab0545d016a58aacfb8dc8199824f46a8e1302074","toolName":"grep","startedAt":"2026-07-16T09:56:31.762Z","args":{"path":"crates/spt-daemon;crates/spt"},"intent":"Finding refresh command test hooks"},"id":"b91a452c","parentId":"f1300f74","timestamp":"2026-07-16T09:56:31.762Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_F176zZdk76ax6zGkzMRQdCkH|fc_0b42584a8ab0545d016a58aacfb8e881998186d2592e75bef9","toolName":"read","startedAt":"2026-07-16T09:56:31.762Z","args":{"path":".github/workflows/ci.yml"},"intent":"Reading CI heavy command fully"},"id":"d96accc2","parentId":"b91a452c","timestamp":"2026-07-16T09:56:31.762Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_im8YAVOrWYaYmYTFo1OsQ7Gi|fc_0b42584a8ab0545d016a58aacfb8ec81999aa9a5a256acf779","toolName":"read","startedAt":"2026-07-16T09:56:31.762Z","args":{"path":"docs/FAULT-MATRIX.md"},"intent":"Reading fault matrix restart rows"},"id":"f9d9fdff","parentId":"d96accc2","timestamp":"2026-07-16T09:56:31.762Z"}
{"type":"message","id":"167f45ee","parentId":"f9d9fdff","timestamp":"2026-07-16T09:56:31.764Z","message":{"role":"toolResult","toolCallId":"call_4QkvnGmHOyuos7jlWIaZK49a|fc_0b42584a8ab0545d016a58aacfb8d08199865be776c32d8299","toolName":"read","content":[{"type":"text","text":"# LIFECYCLE-TRUTH W3 — WAVE GATE REPORT (todlando, 2026-07-07)\n\n**Wave head: `e75e856`** (branch `lifecycle-truth`, pushed). W3 COMPLETE — all 6 REQs built.\nRequesting doyle's gate. Local gate GREEN; Linux leg is doyle's/deployah's.\n\n## W3 ledger (all pushed)\n| REQ | SHA | stages |\n|-----|-----|--------|\n| REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL | `049b2be` | impl+unit+int |\n| REQ-DAEMON-STDERR-PERSIST | `32c789e` | impl+unit |\n| REQ-DAEMON-STOP-LIVE-SESSION-WARN | `ff6ebeb` (+int fix `8931680`) | impl+unit+int |\n| REQ-RC-RECONNECT-TRUTH | `3d632b9` | impl+unit+int |\n| REQ-UPDATE-ONE-SHOT-FINISH | `e85da91` | impl+unit |\n| REQ-DIGEST-GENERATION-SUPERSEDE | `7e37e37` | impl+unit+int |\n| **REQ-UPDATE-PROMOTE-DRAINED** | **`e75e856`** | **impl+unit+int** |\n\n## REQ-UPDATE-PROMOTE-DRAINED @e75e856 (mechanic-d — closes the W2 15s residual, KH 7.36)\n\n**Root closed:** W2's stall-evict only BOUNDED the update-apply false-promote window to\n`BRAIN_WRITE_DEADLINE` (15s); a new brain could signal `brain.ready` inside that window\nwhile the OLD generation's local controller conn was still wedged → apply \"promoted\" onto\na frozen control plane (the 22:47 incident). `brain.ready ≠ subscribers drained`.\n\n**Fix — DRAINED precondition on the promotion gate:**\n- `brainproc.rs run_trial`: LATCH `ready_seen` (a ready-then-exit candidate is not demoted;\n  `child.try_wait()` consulted only while `!ready_seen`), and return `Promoted` only on\n  `ready_seen && env.old_gen_drained()`. Ready-but-never-drained elapses the window →\n  `WindowElapsedAlive` → kill + rollback (conservative; never a false-promote onto a wedge).\n  Normal case drains well inside the window (W2 bounds a real drain to 15s < the 30s window);\n  a cleanly-closed old conn drains at once.\n- New `TrialEnv::old_gen_drained()` reads BROKER TRUTH in-process (no brain round-trip):\n  `Broker::any_local_controller_wedged()` — is any hosted session's LOCAL (`by:None`, the\n  brain's own) controller conn blocked-write past the brain-write deadline? A killed old gen\n  whose conn is black-holed reads wedged until the W2 stall-evict releases it; a cleanly-closed\n  conn is absent. **REMOTE conns (`by:Some`) are EXCLUDED** — that is the W2 concern, not an\n  old-gen brain conn (a stuck remote viewer must never gate a brain promotion).\n  `NoTrialEnv` + a net-less/bind-failed broker degrade to `drained=true` (no control plane to\n  gate — the normal fast promote on `brain.ready` is preserved). `ProductionTrialEnv` holds\n  `Option<Arc<Broker>>`, threaded from `daemon.rs` (`broker_handle.clone()`).\n- `broker.rs OutputLog::local_controller_wedged(now, deadline)`: `by.is_none() && stalled`.\n\n**Off-lock / lock-order proof:** `any_local_controller_wedged` follows the established\nsessions→log lock order (the KIND_SESSIONS reap path), reads only `write_blocked_since` under\neach log lock, does NO I/O under any lock. A poisoned log lock reads NOT-wedged (fail toward\npromotion — never a permanent gate that strands an apply).\n\n**RED-first PROVEN:**\n- Int (`tests/false_promote.rs`, REAL broker): a flood child wedges the spawner's LOCAL\n  controller conn (`SPT_BRAIN_WRITE_DEADLINE_MS` shrunk ~400ms). The promotion path does NOT\n  promote a ready candidate while wedged; a second operator's take stall-evicts the wedged conn\n  broker-side, and the latched-ready candidate then promotes. Drop the `&& old_gen_drained()`\n  gate → `BRAIN_PROMOTED: candidate v9 ... swap accepted` onto the wedge (the 22:47 repro).\n- Unit (brainproc): `ready_but_old_gen_never_drains_does_not_promote_rolls_back` (RED-first —\n  without the gate it takes the promote path) + `ready_candidate_promotes_once_old_gen_drains`\n  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`\n  (by:None only; remote + idle excluded).\n\n## Local gate (this box, HFENDULEAM — Windows)\n- clippy `-p spt-daemon --all-targets`: clean.\n- **Full-workspace nextest (post clean rebuild, `CARGO_INCREMENTAL=0`): 1692/1692 passed**\n  (8 leaky, 1 skip), exit 0. — no-fail-fast.\n- `xtask check` (docs-drift + token, `CARGO_TARGET_DIR` unset): OK.\n- traceable-reqs check: exit 0; REQ-UPDATE-PROMOTE-DRAINED `+impl +unit +int`.\n- Doyle staged condition — grep OLD rc auto-start assertions: CLEAN (every `rc.rs` reference\n  asserts the NEW connect-only truth; the other hits are unrelated `api` startup hygiene).\n\n## Gate gotcha (for the isolated-worktree rig)\nFull-workspace nextest leaks `target\\debug\\spt.exe` dev-daemons from e2e teardown gaps → a\nfollow-up build hits `os error 5` (Access denied) removing the pinned exe. Kill SCOPED (only\nprocesses whose Path == `<this repo>\\target\\debug\\spt.exe`; NEVER the `AppData\\Local\\spt-core\\bin`\nlive plugin listeners) between build steps. (Seen twice this gate; killed 8 then 4.)\n\n## KH 7.36\nUpdated: the W2 residual is now **CLOSED by W3** (`REQ-UPDATE-PROMOTE-DRAINED`), not just bounded.\n"}],"details":{"displayContent":{"text":"# LIFECYCLE-TRUTH W3 — WAVE GATE REPORT (todlando, 2026-07-07)\r\n\r\n**Wave head: `e75e856`** (branch `lifecycle-truth`, pushed). W3 COMPLETE — all 6 REQs built.\r\nRequesting doyle's gate. Local gate GREEN; Linux leg is doyle's/deployah's.\r\n\r\n## W3 ledger (all pushed)\r\n| REQ | SHA | stages |\r\n|-----|-----|--------|\r\n| REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL | `049b2be` | impl+unit+int |\r\n| REQ-DAEMON-STDERR-PERSIST | `32c789e` | impl+unit |\r\n| REQ-DAEMON-STOP-LIVE-SESSION-WARN | `ff6ebeb` (+int fix `8931680`) | impl+unit+int |\r\n| REQ-RC-RECONNECT-TRUTH | `3d632b9` | impl+unit+int |\r\n| REQ-UPDATE-ONE-SHOT-FINISH | `e85da91` | impl+unit |\r\n| REQ-DIGEST-GENERATION-SUPERSEDE | `7e37e37` | impl+unit+int |\r\n| **REQ-UPDATE-PROMOTE-DRAINED** | **`e75e856`** | **impl+unit+int** |\r\n\r\n## REQ-UPDATE-PROMOTE-DRAINED @e75e856 (mechanic-d — closes the W2 15s residual, KH 7.36)\r\n\r\n**Root closed:** W2's stall-evict only BOUNDED the update-apply false-promote window to\r\n`BRAIN_WRITE_DEADLINE` (15s); a new brain could signal `brain.ready` inside that window\r\nwhile the OLD generation's local controller conn was still wedged → apply \"promoted\" onto\r\na frozen control plane (the 22:47 incident). `brain.ready ≠ subscribers drained`.\r\n\r\n**Fix — DRAINED precondition on the promotion gate:**\r\n- `brainproc.rs run_trial`: LATCH `ready_seen` (a ready-then-exit candidate is not demoted;\r\n  `child.try_wait()` consulted only while `!ready_seen`), and return `Promoted` only on\r\n  `ready_seen && env.old_gen_drained()`. Ready-but-never-drained elapses the window →\r\n  `WindowElapsedAlive` → kill + rollback (conservative; never a false-promote onto a wedge).\r\n  Normal case drains well inside the window (W2 bounds a real drain to 15s < the 30s window);\r\n  a cleanly-closed old conn drains at once.\r\n- New `TrialEnv::old_gen_drained()` reads BROKER TRUTH in-process (no brain round-trip):\r\n  `Broker::any_local_controller_wedged()` — is any hosted session's LOCAL (`by:None`, the\r\n  brain's own) controller conn blocked-write past the brain-write deadline? A killed old gen\r\n  whose conn is black-holed reads wedged until the W2 stall-evict releases it; a cleanly-closed\r\n  conn is absent. **REMOTE conns (`by:Some`) are EXCLUDED** — that is the W2 concern, not an\r\n  old-gen brain conn (a stuck remote viewer must never gate a brain promotion).\r\n  `NoTrialEnv` + a net-less/bind-failed broker degrade to `drained=true` (no control plane to\r\n  gate — the normal fast promote on `brain.ready` is preserved). `ProductionTrialEnv` holds\r\n  `Option<Arc<Broker>>`, threaded from `daemon.rs` (`broker_handle.clone()`).\r\n- `broker.rs OutputLog::local_controller_wedged(now, deadline)`: `by.is_none() && stalled`.\r\n\r\n**Off-lock / lock-order proof:** `any_local_controller_wedged` follows the established\r\nsessions→log lock order (the KIND_SESSIONS reap path), reads only `write_blocked_since` under\r\neach log lock, does NO I/O under any lock. A poisoned log lock reads NOT-wedged (fail toward\r\npromotion — never a permanent gate that strands an apply).\r\n\r\n**RED-first PROVEN:**\r\n- Int (`tests/false_promote.rs`, REAL broker): a flood child wedges the spawner's LOCAL\r\n  controller conn (`SPT_BRAIN_WRITE_DEADLINE_MS` shrunk ~400ms). The promotion path does NOT\r\n  promote a ready candidate while wedged; a second operator's take stall-evicts the wedged conn\r\n  broker-side, and the latched-ready candidate then promotes. Drop the `&& old_gen_drained()`\r\n  gate → `BRAIN_PROMOTED: candidate v9 ... swap accepted` onto the wedge (the 22:47 repro).\r\n- Unit (brainproc): `ready_but_old_gen_never_drains_does_not_promote_rolls_back` (RED-first —\r\n  without the gate it takes the promote path) + `ready_candidate_promotes_once_old_gen_drains`\r\n  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`\r\n  (by:None only; remote + idle excluded).\r\n\r\n## Local gate (this box, HFENDULEAM — Windows)\r\n- clippy `-p spt-daemon --all-targets`: clean.\r\n- **Full-workspace nextest (post clean rebuild, `CARGO_INCREMENTAL=0`): 1692/1692 passed**\r\n  (8 leaky, 1 skip), exit 0. — no-fail-fast.\r\n- `xtask check` (docs-drift + token, `CARGO_TARGET_DIR` unset): OK.\r\n- traceable-reqs check: exit 0; REQ-UPDATE-PROMOTE-DRAINED `+impl +unit +int`.\r\n- Doyle staged condition — grep OLD rc auto-start assertions: CLEAN (every `rc.rs` reference\r\n  asserts the NEW connect-only truth; the other hits are unrelated `api` startup hygiene).\r\n\r\n## Gate gotcha (for the isolated-worktree rig)\r\nFull-workspace nextest leaks `target\\debug\\spt.exe` dev-daemons from e2e teardown gaps → a\r\nfollow-up build hits `os error 5` (Access denied) removing the pinned exe. Kill SCOPED (only\r\nprocesses whose Path == `<this repo>\\target\\debug\\spt.exe`; NEVER the `AppData\\Local\\spt-core\\bin`\r\nlive plugin listeners) between build steps. (Seen twice this gate; killed 8 then 4.)\r\n\r\n## KH 7.36\r\nUpdated: the W2 residual is now **CLOSED by W3** (`REQ-UPDATE-PROMOTE-DRAINED`), not just bounded.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\W3-WAVE-GATE-REPORT.md"}}},"isError":false,"timestamp":1784195791764}}
{"type":"message","id":"7cc46d2d","parentId":"167f45ee","timestamp":"2026-07-16T09:56:31.764Z","message":{"role":"toolResult","toolCallId":"call_yXvpOgsZrIfb6iARaoT7DKOx|fc_0b42584a8ab0545d016a58aacfb8c881999ebf2606727c991c","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":1784195791764}}
{"type":"message","id":"91463fa6","parentId":"7cc46d2d","timestamp":"2026-07-16T09:56:31.765Z","message":{"role":"toolResult","toolCallId":"call_8GIpIV0zkl3HHhdueAI3PXWQ|fc_0b42584a8ab0545d016a58aacfb8c481998d4479f7a66db7a2","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":1784195791765}}
{"type":"message","id":"ee839e1d","parentId":"91463fa6","timestamp":"2026-07-16T09:56:31.768Z","message":{"role":"toolResult","toolCallId":"call_5gktamwYR0zG4F9X9BFfhwtW|fc_0b42584a8ab0545d016a58aacfb8b481999cd569b94c4287fb","toolName":"read","content":[{"type":"text","text":"# LIFECYCLE-TRUTH — milestone triage + JIT plan (operator-called 2026-07-07)\n\nField-hardening milestone off the 2026-07-06/07 incident night: the v0.27→v0.28 update wedge,\nthe mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake\ndouble-spawn. Every root below is PINNED (rig-reproduced or line-level) — evidence ledger in\ndoyle's findings backlog (seeds #5–#10) and this doc's per-wave notes. Owner: doyle (gate),\nexecutor: todlando. Waves ordered by field pressure × dependency; **mint ALL REQs\n`required_stages=[]` first, activate per wave as started** (traceable rule 5).\n\nGate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,\nnextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change →\ngrep tests for OLD behavior as assertions, shared-seam → run ALL seam tests, gate suites\nFOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag — the earmarked\nREQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.\n\n---\n\n## W1 — psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)\n\n**REQ-ECHO-DROP-DIR-RESOLVE** (impl, unit)\nRoot: `fire_echo` (spt-daemon lifecycle.rs:790) passes the RAW manifest `commune_dir` into\n`run_echo_commune` → echo.rs:115-117 `create_dir_all`+join; relative `.claude` under the\nWMI-launched daemon's System32 cwd → `os error 5` deterministic. Siblings already resolve\ncorrectly (ingest ~:583, `psyche_drop_file` :1072 via `resolve_endpoint_drop_dir(raw, cwd)`).\nFix: fire_echo routes through the SAME resolver; relative-with-no-cwd = SKIP LOUD (stderr),\nnever a raw relative write. Latent-worse variant to kill with it: writable daemon cwd would\nwrite the drop to a WRONG dir silently (echo communes lost, no error).\nUnit: relative dir + cwd → resolved; absolute → as-is; relative no-cwd → loud skip.\nHardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write;\necho claude spawn gets explicit cwd = endpoint cwd (perri ask).\n\n**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)\nRoot: `psyche_host_error` clears only via the pulse-loop `note_turn_outcome` Ok leg\n(lifecycle.rs:1101); a SUCCESSFUL psyche op via checkpoint/wake bypasses it → stale FAILED\nstamp over a healthy psyche (three field confirmations, perri). Fix: every successful psyche\noperation (event turn, checkpoint/wake synthesis, signoff echo) clears the stamp.\nUnit: checkpoint-success path clears a pre-existing stamp.\n\n**REQ-PSYCHE-ROLE-OPTIONAL-SKIP** (impl, unit)\nRoot (perri filing, recovered): manifest with NO `[session.echo_commune]` role → commune-sync\nhard-fails → 3-strike stamps the host (\"manifest declares no [session.echo_commune] role\").\nPublished contract presents the role as optional template. Fix: missing OPTIONAL role = SKIP\n(no strike, debug-level note), not a turn failure.\nUnit: role-less manifest ticks strike-free.\n\nField acceptance (doyle, gate-time): after fix lands on this node, next commune clears BOTH\nlive stamps (doyle + perri) with zero manual intervention.\n\n## W2 — broker viewer/brain decouple (flagship — the update wedge)\n\n**REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE** (doc, impl, unit, int)\nRoot, rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): PTY viewer fan-out\nand control mutations depend synchronously on a live, DRAINING brain — brain-subscriber\nsession-output writes ride UNDER the per-session log lock (broker.rs:19-20). Failed writes are\nhandled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within\nseconds: attached rc output freezes; detach does NOT release the control stamp (release routes\nthrough the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays\nhealthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled\nor slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\nwedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply \"promoted\"\nwhile frozen.\nFix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log\nlock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already\nbuffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control\nstamp release/take must complete against the BROKER without brain round-trip (or bounded with\nloud timeout). doc = KNOWN-HAZARDS entry.\nInt (the rig, encoded): suspend the brain child mid-session → attached viewer ticks CONTINUE +\n`rc --take` completes; resume → no output lost (cursor replay).\n\n## W3 — update finish truth (the \"seamless live update\" cornerstone)\n\n**REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL** (impl, unit, int)\nRig-proven: `daemon stop`+`start` (the apply notice's OWN instruction) kills every hosted\nendpoint; they stay OFFLINE after start (no resurrection). Fix: daemon start RE-RUNS\npreviously-online spt-hosted endpoints (records exist: info.json status + adapter + cwd);\nalternatively stop performs a drain/handoff. Scope ruling (doyle): re-run-on-start, marked\n`start-reason=daemon-restart`; agents' minds ride psyche re-host as today.\nInt: endpoint online → daemon stop → start → endpoint back ONLINE, same id, harness respawned.\n\n**REQ-UPDATE-ONE-SHOT-FINISH** (impl, unit)\nOperator wart: `update fetch`/`update apply` run `ensure_daemon_announced` (cli.rs:4386) →\non a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain\npair + a manual bounce. Fix: apply works daemonless (swap + record, next start runs new bytes);\n`update apply --finish` (name subject to docs-token gate) completes the cycle: swap → brain\ncycle → broker restart onto new bytes (riding W3's endpoint re-run so the restart is no longer\na massacre). CLI change → xtask docs gen, no internal codes in clap ///.\n\n**REQ-DAEMON-STOP-LIVE-SESSION-WARN** (impl, unit) — promoted old follow-wave seed: stop with\nlive hosted sessions warns + requires --force (or names the sessions it will kill).\n\n**REQ-RC-RECONNECT-TRUTH** (impl, unit)\nRig-proven roots of the operator's long-standing \"stop 2-4 times\" bug: (a) an rc client's\nreconnect loop AUTO-LAUNCHES a daemon via WMI (rig: DAEMON_LAUNCH_VIA_WMI from the rc) —\nresurrection fights the operator's stops; (b) rc freezes at \"Reconnecting to local daemon…\"\nforever when its session died with the broker. Fix: rc NEVER auto-starts a daemon (reconnect\nonly to an already-up broker; loud \"session lost — daemon down\" exit otherwise), bounded\nreconnect with visible countdown.\n\n**REQ-DAEMON-STDERR-PERSIST** (impl, unit)\nObservability: detached daemon nulls stdio → the incident window left ZERO logs (both RCAs ran\nblind; rigs had to recreate everything). Fix: broker + brain stderr tee to a rotating file\nunder SPT_HOME (size-capped, e.g. 2×5MB), stamped per generation. KNOWN-HAZARDS note: never\ninherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.\n\n## W4 — spawn/wake + listener lifecycle\n\n**REQ-SPAWN-COLLISION-GUARD-LIVE-DUP** (impl, unit, int)\nRoot (perri parentage + recovered filing): the WAKE/RESUME respawn seam processed ONE wake\nTWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume\n<sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. Damage mode:\nduplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a\nfresh /clear rotation → injects routed to the contended record and lost). Fix: single-flight\nwake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second\n…\nInt: two concurrent wake requests → exactly one launch tree.\n…\n\n…\n  (bump-ON-milestone-branch pattern, deployah drives).\n\n[Showing lines 1-113 of 219. Use :114 to continue]"}],"details":{"truncation":{"content":"# LIFECYCLE-TRUTH — milestone triage + JIT plan (operator-called 2026-07-07)\r\n\r\nField-hardening milestone off the 2026-07-06/07 incident night: the v0.27→v0.28 update wedge,\r\nthe mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake\r\ndouble-spawn. Every root below is PINNED (rig-reproduced or line-level) — evidence ledger in\r\ndoyle's findings backlog (seeds #5–#10) and this doc's per-wave notes. Owner: doyle (gate),\r\nexecutor: todlando. Waves ordered by field pressure × dependency; **mint ALL REQs\r\n`required_stages=[]` first, activate per wave as started** (traceable rule 5).\r\n\r\nGate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,\r\nnextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change →\r\ngrep tests for OLD behavior as assertions, shared-seam → run ALL seam tests, gate suites\r\nFOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag — the earmarked\r\nREQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.\r\n\r\n---\r\n\r\n## W1 — psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)\r\n\r\n**REQ-ECHO-DROP-DIR-RESOLVE** (impl, unit)\r\nRoot: `fire_echo` (spt-daemon lifecycle.rs:790) passes the RAW manifest `commune_dir` into\r\n`run_echo_commune` → echo.rs:115-117 `create_dir_all`+join; relative `.claude` under the\r\nWMI-launched daemon's System32 cwd → `os error 5` deterministic. Siblings already resolve\r\ncorrectly (ingest ~:583, `psyche_drop_file` :1072 via `resolve_endpoint_drop_dir(raw, cwd)`).\r\nFix: fire_echo routes through the SAME resolver; relative-with-no-cwd = SKIP LOUD (stderr),\r\nnever a raw relative write. Latent-worse variant to kill with it: writable daemon cwd would\r\nwrite the drop to a WRONG dir silently (echo communes lost, no error).\r\nUnit: relative dir + cwd → resolved; absolute → as-is; relative no-cwd → loud skip.\r\nHardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write;\r\necho claude spawn gets explicit cwd = endpoint cwd (perri ask).\r\n\r\n**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)\r\nRoot: `psyche_host_error` clears only via the pulse-loop `note_turn_outcome` Ok leg\r\n(lifecycle.rs:1101); a SUCCESSFUL psyche op via checkpoint/wake bypasses it → stale FAILED\r\nstamp over a healthy psyche (three field confirmations, perri). Fix: every successful psyche\r\noperation (event turn, checkpoint/wake synthesis, signoff echo) clears the stamp.\r\nUnit: checkpoint-success path clears a pre-existing stamp.\r\n\r\n**REQ-PSYCHE-ROLE-OPTIONAL-SKIP** (impl, unit)\r\nRoot (perri filing, recovered): manifest with NO `[session.echo_commune]` role → commune-sync\r\nhard-fails → 3-strike stamps the host (\"manifest declares no [session.echo_commune] role\").\r\nPublished contract presents the role as optional template. Fix: missing OPTIONAL role = SKIP\r\n(no strike, debug-level note), not a turn failure.\r\nUnit: role-less manifest ticks strike-free.\r\n\r\nField acceptance (doyle, gate-time): after fix lands on this node, next commune clears BOTH\r\nlive stamps (doyle + perri) with zero manual intervention.\r\n\r\n## W2 — broker viewer/brain decouple (flagship — the update wedge)\r\n\r\n**REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE** (doc, impl, unit, int)\r\nRoot, rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): PTY viewer fan-out\r\nand control mutations depend synchronously on a live, DRAINING brain — brain-subscriber\r\nsession-output writes ride UNDER the per-session log lock (broker.rs:19-20). Failed writes are\r\nhandled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within\r\nseconds: attached rc output freezes; detach does NOT release the control stamp (release routes\r\nthrough the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays\r\nhealthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled\r\nor slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\r\nwedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply \"promoted\"\r\nwhile frozen.\r\nFix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log\r\nlock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already\r\nbuffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control\r\nstamp release/take must complete against the BROKER without brain round-trip (or bounded with\r\nloud timeout). doc = KNOWN-HAZARDS entry.\r\nInt (the rig, encoded): suspend the brain child mid-session → attached viewer ticks CONTINUE +\r\n`rc --take` completes; resume → no output lost (cursor replay).\r\n\r\n## W3 — update finish truth (the \"seamless live update\" cornerstone)\r\n\r\n**REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL** (impl, unit, int)\r\nRig-proven: `daemon stop`+`start` (the apply notice's OWN instruction) kills every hosted\r\nendpoint; they stay OFFLINE after start (no resurrection). Fix: daemon start RE-RUNS\r\npreviously-online spt-hosted endpoints (records exist: info.json status + adapter + cwd);\r\nalternatively stop performs a drain/handoff. Scope ruling (doyle): re-run-on-start, marked\r\n`start-reason=daemon-restart`; agents' minds ride psyche re-host as today.\r\nInt: endpoint online → daemon stop → start → endpoint back ONLINE, same id, harness respawned.\r\n\r\n**REQ-UPDATE-ONE-SHOT-FINISH** (impl, unit)\r\nOperator wart: `update fetch`/`update apply` run `ensure_daemon_announced` (cli.rs:4386) →\r\non a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain\r\npair + a manual bounce. Fix: apply works daemonless (swap + record, next start runs new bytes);\r\n`update apply --finish` (name subject to docs-token gate) completes the cycle: swap → brain\r\ncycle → broker restart onto new bytes (riding W3's endpoint re-run so the restart is no longer\r\na massacre). CLI change → xtask docs gen, no internal codes in clap ///.\r\n\r\n**REQ-DAEMON-STOP-LIVE-SESSION-WARN** (impl, unit) — promoted old follow-wave seed: stop with\r\nlive hosted sessions warns + requires --force (or names the sessions it will kill).\r\n\r\n**REQ-RC-RECONNECT-TRUTH** (impl, unit)\r\nRig-proven roots of the operator's long-standing \"stop 2-4 times\" bug: (a) an rc client's\r\nreconnect loop AUTO-LAUNCHES a daemon via WMI (rig: DAEMON_LAUNCH_VIA_WMI from the rc) —\r\nresurrection fights the operator's stops; (b) rc freezes at \"Reconnecting to local daemon…\"\r\nforever when its session died with the broker. Fix: rc NEVER auto-starts a daemon (reconnect\r\nonly to an already-up broker; loud \"session lost — daemon down\" exit otherwise), bounded\r\nreconnect with visible countdown.\r\n\r\n**REQ-DAEMON-STDERR-PERSIST** (impl, unit)\r\nObservability: detached daemon nulls stdio → the incident window left ZERO logs (both RCAs ran\r\nblind; rigs had to recreate everything). Fix: broker + brain stderr tee to a rotating file\r\nunder SPT_HOME (size-capped, e.g. 2×5MB), stamped per generation. KNOWN-HAZARDS note: never\r\ninherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.\r\n\r\n## W4 — spawn/wake + listener lifecycle\r\n\r\n**REQ-SPAWN-COLLISION-GUARD-LIVE-DUP** (impl, unit, int)\r\nRoot (perri parentage + recovered filing): the WAKE/RESUME respawn seam processed ONE wake\r\nTWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume\r\n<sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. Damage mode:\r\nduplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a\r\nfresh /clear rotation → injects routed to the contended record and lost). Fix: single-flight\r\nwake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second\r","truncated":true,"truncatedBy":"lines","totalLines":219,"totalBytes":7850,"outputLines":113,"outputBytes":7850,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# LIFECYCLE-TRUTH — milestone triage + JIT plan (operator-called 2026-07-07)\r\n\r\nField-hardening milestone off the 2026-07-06/07 incident night: the v0.27→v0.28 update wedge,\r\nthe mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake\r\ndouble-spawn. Every root below is PINNED (rig-reproduced or line-level) — evidence ledger in\r\ndoyle's findings backlog (seeds #5–#10) and this doc's per-wave notes. Owner: doyle (gate),\r\nexecutor: todlando. Waves ordered by field pressure × dependency; **mint ALL REQs\r\n`required_stages=[]` first, activate per wave as started** (traceable rule 5).\r\n\r\nGate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,\r\nnextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change →\r\ngrep tests for OLD behavior as assertions, shared-seam → run ALL seam tests, gate suites\r\nFOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag — the earmarked\r\nREQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.\r\n\r\n---\r\n\r\n## W1 — psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)\r\n\r\n**REQ-ECHO-DROP-DIR-RESOLVE** (impl, unit)\r\nRoot: `fire_echo` (spt-daemon lifecycle.rs:790) passes the RAW manifest `commune_dir` into\r\n`run_echo_commune` → echo.rs:115-117 `create_dir_all`+join; relative `.claude` under the\r\nWMI-launched daemon's System32 cwd → `os error 5` deterministic. Siblings already resolve\r\ncorrectly (ingest ~:583, `psyche_drop_file` :1072 via `resolve_endpoint_drop_dir(raw, cwd)`).\r\nFix: fire_echo routes through the SAME resolver; relative-with-no-cwd = SKIP LOUD (stderr),\r\nnever a raw relative write. Latent-worse variant to kill with it: writable daemon cwd would\r\nwrite the drop to a WRONG dir silently (echo communes lost, no error).\r\nUnit: relative dir + cwd → resolved; absolute → as-is; relative no-cwd → loud skip.\r\nHardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write;\r\necho claude spawn gets explicit cwd = endpoint cwd (perri ask).\r\n\r\n**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)\r\nRoot: `psyche_host_error` clears only via the pulse-loop `note_turn_outcome` Ok leg\r\n(lifecycle.rs:1101); a SUCCESSFUL psyche op via checkpoint/wake bypasses it → stale FAILED\r\nstamp over a healthy psyche (three field confirmations, perri). Fix: every successful psyche\r\noperation (event turn, checkpoint/wake synthesis, signoff echo) clears the stamp.\r\nUnit: checkpoint-success path clears a pre-existing stamp.\r\n\r\n**REQ-PSYCHE-ROLE-OPTIONAL-SKIP** (impl, unit)\r\nRoot (perri filing, recovered): manifest with NO `[session.echo_commune]` role → commune-sync\r\nhard-fails → 3-strike stamps the host (\"manifest declares no [session.echo_commune] role\").\r\nPublished contract presents the role as optional template. Fix: missing OPTIONAL role = SKIP\r\n(no strike, debug-level note), not a turn failure.\r\nUnit: role-less manifest ticks strike-free.\r\n\r\nField acceptance (doyle, gate-time): after fix lands on this node, next commune clears BOTH\r\nlive stamps (doyle + perri) with zero manual intervention.\r\n\r\n## W2 — broker viewer/brain decouple (flagship — the update wedge)\r\n\r\n**REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE** (doc, impl, unit, int)\r\nRoot, rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): PTY viewer fan-out\r\nand control mutations depend synchronously on a live, DRAINING brain — brain-subscriber\r\nsession-output writes ride UNDER the per-session log lock (broker.rs:19-20). Failed writes are\r\nhandled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within\r\nseconds: attached rc output freezes; detach does NOT release the control stamp (release routes\r\nthrough the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays\r\nhealthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled\r\nor slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\r\nwedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply \"promoted\"\r\nwhile frozen.\r\nFix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log\r\nlock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already\r\nbuffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control\r\nstamp release/take must complete against the BROKER without brain round-trip (or bounded with\r\nloud timeout). doc = KNOWN-HAZARDS entry.\r\nInt (the rig, encoded): suspend the brain child mid-session → attached viewer ticks CONTINUE +\r\n`rc --take` completes; resume → no output lost (cursor replay).\r\n\r\n## W3 — update finish truth (the \"seamless live update\" cornerstone)\r\n\r\n**REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL** (impl, unit, int)\r\nRig-proven: `daemon stop`+`start` (the apply notice's OWN instruction) kills every hosted\r\nendpoint; they stay OFFLINE after start (no resurrection). Fix: daemon start RE-RUNS\r\npreviously-online spt-hosted endpoints (records exist: info.json status + adapter + cwd);\r\nalternatively stop performs a drain/handoff. Scope ruling (doyle): re-run-on-start, marked\r\n`start-reason=daemon-restart`; agents' minds ride psyche re-host as today.\r\nInt: endpoint online → daemon stop → start → endpoint back ONLINE, same id, harness respawned.\r\n\r\n**REQ-UPDATE-ONE-SHOT-FINISH** (impl, unit)\r\nOperator wart: `update fetch`/`update apply` run `ensure_daemon_announced` (cli.rs:4386) →\r\non a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain\r\npair + a manual bounce. Fix: apply works daemonless (swap + record, next start runs new bytes);\r\n`update apply --finish` (name subject to docs-token gate) completes the cycle: swap → brain\r\ncycle → broker restart onto new bytes (riding W3's endpoint re-run so the restart is no longer\r\na massacre). CLI change → xtask docs gen, no internal codes in clap ///.\r\n\r\n**REQ-DAEMON-STOP-LIVE-SESSION-WARN** (impl, unit) — promoted old follow-wave seed: stop with\r\nlive hosted sessions warns + requires --force (or names the sessions it will kill).\r\n\r\n**REQ-RC-RECONNECT-TRUTH** (impl, unit)\r\nRig-proven roots of the operator's long-standing \"stop 2-4 times\" bug: (a) an rc client's\r\nreconnect loop AUTO-LAUNCHES a daemon via WMI (rig: DAEMON_LAUNCH_VIA_WMI from the rc) —\r\nresurrection fights the operator's stops; (b) rc freezes at \"Reconnecting to local daemon…\"\r\nforever when its session died with the broker. Fix: rc NEVER auto-starts a daemon (reconnect\r\nonly to an already-up broker; loud \"session lost — daemon down\" exit otherwise), bounded\r\nreconnect with visible countdown.\r\n\r\n**REQ-DAEMON-STDERR-PERSIST** (impl, unit)\r\nObservability: detached daemon nulls stdio → the incident window left ZERO logs (both RCAs ran\r\nblind; rigs had to recreate everything). Fix: broker + brain stderr tee to a rotating file\r\nunder SPT_HOME (size-capped, e.g. 2×5MB), stamped per generation. KNOWN-HAZARDS note: never\r\ninherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.\r\n\r\n## W4 — spawn/wake + listener lifecycle\r\n\r\n**REQ-SPAWN-COLLISION-GUARD-LIVE-DUP** (impl, unit, int)\r\nRoot (perri parentage + recovered filing): the WAKE/RESUME respawn seam processed ONE wake\r\nTWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume\r\n<sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. Damage mode:\r\nduplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a\r\nfresh /clear rotation → injects routed to the contended record and lost). Fix: single-flight\r\nwake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second\r\n…\nInt: two concurrent wake requests → exactly one launch tree.\n…\n\n…\n  (bump-ON-milestone-branch pattern, deployah drives).","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,null,115,null,125,null,218]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":219,"totalBytes":7850,"outputLines":113,"outputBytes":7850,"shownRange":{"start":1,"end":113},"nextOffset":114}}},"isError":false,"timestamp":1784195791768}}
{"type":"message","id":"60183806","parentId":"ee839e1d","timestamp":"2026-07-16T09:56:31.771Z","message":{"role":"toolResult","toolCallId":"call_im8YAVOrWYaYmYTFo1OsQ7Gi|fc_0b42584a8ab0545d016a58aacfb8ec81999aa9a5a256acf779","toolName":"read","content":[{"type":"text","text":"# Fault-injection acceptance matrix\n\n**Why this exists.** ADR-0002 collapsed networking into the one per-machine daemon\n(SERIOUS #6, Stage-A red team): a networking bug and a PTY bug now share a failure\ndomain. The collapse is locked — the owed mitigation is this matrix: inject a fault\ninto each subsystem **independently**, observe the blast radius (what else breaks)\nand the recovery (what brings it back), and grow the proven rows until every\nsubsystem has injection evidence. Begun at M4-D4 (per `M4-D4-PLAN.md` piece 5);\nrows land as their machinery lands.\n\n**How to read it.** *Fault* = what is injected. *Blast radius* = the components that\ndegrade, by design. *Survives* = what must keep working untouched. *Recovery* = the\npath back. *Evidence* = the test (or gap) backing the row. Rows marked **planned**\nare accepted debt — tracked here so the matrix is the single place to look.\n\n## Proven rows (injection evidence in-tree)\n\n| # | Fault injected | Blast radius (by design) | Survives untouched | Recovery | Evidence |\n|---|---|---|---|---|---|\n| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\n| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\n| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\n| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\n| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\n| 6 | **Registry feed: stale/replayed update over the wire** | None — update dropped | Stored registry state (newer epoch holds) | None needed (epoch lease absorbs it) | `tests/replicate.rs::registries_converge_over_the_wire_and_the_lease_holds`; `replicate.rs` unit tests |\n| 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| 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| 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` reconnect units (`reconnect_class_only_severed_transport_redrives`, `serve_eof_severed_vs_refused`, `reconnect_banner_clears_and_centers`) |\n| 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| 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| 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| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (never renames garbage over it) | `tests/xfer.rs::push_survives_target_brain_restart_exactly_once`; `xfer.rs::recv_state_chunks_idempotent_and_commit_replays_safely` |\n| 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::torn_pull_recovers_by_repulling` |\n| 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 | `tests/sync.rs::concurrent_writes_reconcile_on_elected_node_and_converge`; `spt-store::syncmerge` unit suite |\n| 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| 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 pass | `spt-daemon reconcile.rs::double_active_tiebreaks_deterministically` / `fallback_lowest_non_offline`; `contextmark.rs::merge_decisions` (concurrent never auto-picks) |\n| 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-propagates | `tests/propagate.rs::tampered_relay_artifact_is_rejected_and_never_staged` |\n| 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| 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| 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_corrupt_stage_offers_nothing` |\n\n| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psyrelay.rs::spoofed_routing_is_stripped_and_restamped`; `spt-live outbound.rs::spoofed_routing_attrs_are_unrepresentable` |\n| 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| 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| 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| 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| 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_idempotent_and_commutative` |\n| 28 | **Untrusted origin injects notif records** (unpaired peer, or a `home`-trusted peer spoofing into `work`) | None — dropped fail-closed at the apply gate, zero rows written; the gate's subject is the handshake-proven stream-table origin, never payload bytes (KH 7.5) | The spool (nothing materialized); every subnet the origin isn't trusted in (trust is per-subnet) | None needed; pair the node to entitle it | `tests/notifsync.rs` (gate-negative leg); `notifsync.rs::untrusted_and_cross_subnet_origins_are_dropped` |\n| 29 | **Notif record for a non-member subnet** (a peer feeds rows for a subnet this node never joined) | None — dropped fail-closed; the subnet is never materialized locally (the REQ-INST-13 posture: unconfigured replicates nowhere) | The member subnets' spools; the feed (other records still apply) | None needed | `notifsync.rs::non_member_subnet_record_never_materializes` |\n\n## Planned rows (accepted debt — no injection evidence yet)\n\n| # | Fault to inject | Expected blast radius | Expected survival | Target |\n|---|---|---|---|---|\n| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n| P8 | **Receiver crash between a live-TCP WAN delivery and its op-id claim** | That one in-flight op re-delivers on replay (at-least-once window — chosen over claim-first, whose crash gap silently *drops*; see `wan.rs` module docs) | Spool path (claim+row atomic); all other ops | live-listener durable receipt, if the window ever bites |\n\n**Maintenance rule:** when a planned row gains a real injection test, move it up with\nits evidence path. When new subsystems land (D5 WAN delivery, D6 Psyche sync), add\ntheir fault rows *in the same change* — a subsystem without a row here is a review\nfinding.\n"}],"details":{"displayContent":{"text":"# Fault-injection acceptance matrix\r\n\r\n**Why this exists.** ADR-0002 collapsed networking into the one per-machine daemon\r\n(SERIOUS #6, Stage-A red team): a networking bug and a PTY bug now share a failure\r\ndomain. The collapse is locked — the owed mitigation is this matrix: inject a fault\r\ninto each subsystem **independently**, observe the blast radius (what else breaks)\r\nand the recovery (what brings it back), and grow the proven rows until every\r\nsubsystem has injection evidence. Begun at M4-D4 (per `M4-D4-PLAN.md` piece 5);\r\nrows land as their machinery lands.\r\n\r\n**How to read it.** *Fault* = what is injected. *Blast radius* = the components that\r\ndegrade, by design. *Survives* = what must keep working untouched. *Recovery* = the\r\npath back. *Evidence* = the test (or gap) backing the row. Rows marked **planned**\r\nare accepted debt — tracked here so the matrix is the single place to look.\r\n\r\n## Proven rows (injection evidence in-tree)\r\n\r\n| # | Fault injected | Blast radius (by design) | Survives untouched | Recovery | Evidence |\r\n|---|---|---|---|---|---|\r\n| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\r\n| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\r\n| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\r\n| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\r\n| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\r\n| 6 | **Registry feed: stale/replayed update over the wire** | None — update dropped | Stored registry state (newer epoch holds) | None needed (epoch lease absorbs it) | `tests/replicate.rs::registries_converge_over_the_wire_and_the_lease_holds`; `replicate.rs` unit tests |\r\n| 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` |\r\n| 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 |\r\n| 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` reconnect units (`reconnect_class_only_severed_transport_redrives`, `serve_eof_severed_vs_refused`, `reconnect_banner_clears_and_centers`) |\r\n| 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 |\r\n| 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` |\r\n| 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` |\r\n| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (never renames garbage over it) | `tests/xfer.rs::push_survives_target_brain_restart_exactly_once`; `xfer.rs::recv_state_chunks_idempotent_and_commit_replays_safely` |\r\n| 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::torn_pull_recovers_by_repulling` |\r\n| 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 | `tests/sync.rs::concurrent_writes_reconcile_on_elected_node_and_converge`; `spt-store::syncmerge` unit suite |\r\n| 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` |\r\n| 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 pass | `spt-daemon reconcile.rs::double_active_tiebreaks_deterministically` / `fallback_lowest_non_offline`; `contextmark.rs::merge_decisions` (concurrent never auto-picks) |\r\n| 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-propagates | `tests/propagate.rs::tampered_relay_artifact_is_rejected_and_never_staged` |\r\n| 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` |\r\n| 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` |\r\n| 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_corrupt_stage_offers_nothing` |\r\n\r\n| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psyrelay.rs::spoofed_routing_is_stripped_and_restamped`; `spt-live outbound.rs::spoofed_routing_attrs_are_unrepresentable` |\r\n| 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` |\r\n| 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` |\r\n| 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` |\r\n| 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` |\r\n| 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_idempotent_and_commutative` |\r\n| 28 | **Untrusted origin injects notif records** (unpaired peer, or a `home`-trusted peer spoofing into `work`) | None — dropped fail-closed at the apply gate, zero rows written; the gate's subject is the handshake-proven stream-table origin, never payload bytes (KH 7.5) | The spool (nothing materialized); every subnet the origin isn't trusted in (trust is per-subnet) | None needed; pair the node to entitle it | `tests/notifsync.rs` (gate-negative leg); `notifsync.rs::untrusted_and_cross_subnet_origins_are_dropped` |\r\n| 29 | **Notif record for a non-member subnet** (a peer feeds rows for a subnet this node never joined) | None — dropped fail-closed; the subnet is never materialized locally (the REQ-INST-13 posture: unconfigured replicates nowhere) | The member subnets' spools; the feed (other records still apply) | None needed | `notifsync.rs::non_member_subnet_record_never_materializes` |\r\n\r\n## Planned rows (accepted debt — no injection evidence yet)\r\n\r\n| # | Fault to inject | Expected blast radius | Expected survival | Target |\r\n|---|---|---|---|---|\r\n| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\r\n| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\r\n| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\r\n| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\r\n| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\r\n| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\r\n| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\r\n| P8 | **Receiver crash between a live-TCP WAN delivery and its op-id claim** | That one in-flight op re-delivers on replay (at-least-once window — chosen over claim-first, whose crash gap silently *drops*; see `wan.rs` module docs) | Spool path (claim+row atomic); all other ops | live-listener durable receipt, if the window ever bites |\r\n\r\n**Maintenance rule:** when a planned row gains a real injection test, move it up with\r\nits evidence path. When new subsystems land (D5 WAN delivery, D6 Psyche sync), add\r\ntheir fault rows *in the same change* — a subsystem without a row here is a review\r\nfinding.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\FAULT-MATRIX.md"}}},"isError":false,"timestamp":1784195791771}}
{"type":"message","id":"3bd78bed","parentId":"60183806","timestamp":"2026-07-16T09:56:31.774Z","message":{"role":"toolResult","toolCallId":"call_F176zZdk76ax6zGkzMRQdCkH|fc_0b42584a8ab0545d016a58aacfb8e881998186d2592e75bef9","toolName":"read","content":[{"type":"text","text":"      - uses: actions/checkout@v5\n      # Linux: link with mold (gcc -fuse-ld=mold). The slow part of a Linux\n      # build is serial linking of the many test/bin targets; mold parallelizes\n      # it. Linux-only + CI-only (set into the job env, never committed to\n      # .cargo/config), so Windows (MSVC linker) and Linux devs without mold are\n      # untouched. See ci-kitsubito-speedup-plan.\n      - name: Use mold linker (Linux)\n        if: runner.os == 'Linux'\n        run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"\n      # `cargo nextest` runs every test across ALL test binaries in ONE parallel\n      # pool — `cargo test` runs the binaries SERIALLY (only threads WITHIN a\n      # binary). nextest also builds the test targets, so the old standalone `cargo\n      # build --all-targets` is dropped (clippy --all-targets below still compiles\n      # every target — examples/benches included — so non-test breakage is caught).\n      #\n      # TWO-PHASE split (correctness > raw speed for the release gate): the heavy\n      # real-broker/PTY/daemon-tree class is timing-sensitive and STARVES when it\n      # shares the global pool with the ~1200 light tests on a saturated self-hosted\n      # box (kitsubito a_journaled_input_wedge subscribed=false; Windows\n      # job_escape_e2e tree-reap timeout). So run the LIGHT pool first at full\n      # parallelism, THEN the HEAVY class on the now-idle box (and serialized by the\n      # heavy-broker-pty test-group). The <HEAVY> filter MUST stay identical in both\n      # `-E` strings below AND in .config/nextest.toml's override — Phase A is the\n      # exact negation of Phase B, so the two phases partition the suite with nothing\n      # dropped or double-run.\n      - name: Test — Phase A (light pool, full parallel)\n        run: cargo nextest run --workspace --no-fail-fast -E 'not ( (package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|r…\n      - name: Test — Phase B (heavy class, serialized, on a now-quiet box)\n        run: cargo nextest run --workspace --no-fail-fast -E '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|run_no_…\n      # nextest does not run doctests — keep them on `cargo test --doc`.\n      - name: Doctests\n        run: cargo test --workspace --doc\n      - name: Clippy (deny warnings)\n        run: cargo clippy --workspace --all-targets -- -D warnings\n\n      # ---- M5-D8a real-shell dogfood E2E (thin cross-repo hook) ----\n…\n      # (only spt-core + the release channel moved orgs).\n\n[Showing lines 149-183 of 529. Use :184 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"      # Linux: link with mold (gcc -fuse-ld=mold). The slow part of a Linux\r\n      # build is serial linking of the many test/bin targets; mold parallelizes\r\n      # it. Linux-only + CI-only (set into the job env, never committed to\r\n      # .cargo/config), so Windows (MSVC linker) and Linux devs without mold are\r\n      # untouched. See ci-kitsubito-speedup-plan.\r\n      - name: Use mold linker (Linux)\r\n        if: runner.os == 'Linux'\r\n        run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"\r\n      # `cargo nextest` runs every test across ALL test binaries in ONE parallel\r\n      # pool — `cargo test` runs the binaries SERIALLY (only threads WITHIN a\r\n      # binary). nextest also builds the test targets, so the old standalone `cargo\r\n      # build --all-targets` is dropped (clippy --all-targets below still compiles\r\n      # every target — examples/benches included — so non-test breakage is caught).\r\n      #\r\n      # TWO-PHASE split (correctness > raw speed for the release gate): the heavy\r\n      # real-broker/PTY/daemon-tree class is timing-sensitive and STARVES when it\r\n      # shares the global pool with the ~1200 light tests on a saturated self-hosted\r\n      # box (kitsubito a_journaled_input_wedge subscribed=false; Windows\r\n      # job_escape_e2e tree-reap timeout). So run the LIGHT pool first at full\r\n      # parallelism, THEN the HEAVY class on the now-idle box (and serialized by the\r\n      # heavy-broker-pty test-group). The <HEAVY> filter MUST stay identical in both\r\n      # `-E` strings below AND in .config/nextest.toml's override — Phase A is the\r\n      # exact negation of Phase B, so the two phases partition the suite with nothing\r\n      # dropped or double-run.\r\n      - name: Test — Phase A (light pool, full parallel)\r\n        run: cargo nextest run --workspace --no-fail-fast -E 'not ( (package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|r…\n      - name: Test — Phase B (heavy class, serialized, on a now-quiet box)\r\n        run: cargo nextest run --workspace --no-fail-fast -E '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|run_no_…\n      # nextest does not run doctests — keep them on `cargo test --doc`.\r\n      - name: Doctests\r\n        run: cargo test --workspace --doc\r\n      - name: Clippy (deny warnings)\r\n        run: cargo clippy --workspace --all-targets -- -D warnings\r\n\r\n      # ---- M5-D8a real-shell dogfood E2E (thin cross-repo hook) ----\r","truncated":true,"truncatedBy":"lines","totalLines":381,"totalBytes":3847,"outputLines":35,"outputBytes":3847,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"      - uses: actions/checkout@v5\n      # Linux: link with mold (gcc -fuse-ld=mold). The slow part of a Linux\r\n      # build is serial linking of the many test/bin targets; mold parallelizes\r\n      # it. Linux-only + CI-only (set into the job env, never committed to\r\n      # .cargo/config), so Windows (MSVC linker) and Linux devs without mold are\r\n      # untouched. See ci-kitsubito-speedup-plan.\r\n      - name: Use mold linker (Linux)\r\n        if: runner.os == 'Linux'\r\n        run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"\r\n      # `cargo nextest` runs every test across ALL test binaries in ONE parallel\r\n      # pool — `cargo test` runs the binaries SERIALLY (only threads WITHIN a\r\n      # binary). nextest also builds the test targets, so the old standalone `cargo\r\n      # build --all-targets` is dropped (clippy --all-targets below still compiles\r\n      # every target — examples/benches included — so non-test breakage is caught).\r\n      #\r\n      # TWO-PHASE split (correctness > raw speed for the release gate): the heavy\r\n      # real-broker/PTY/daemon-tree class is timing-sensitive and STARVES when it\r\n      # shares the global pool with the ~1200 light tests on a saturated self-hosted\r\n      # box (kitsubito a_journaled_input_wedge subscribed=false; Windows\r\n      # job_escape_e2e tree-reap timeout). So run the LIGHT pool first at full\r\n      # parallelism, THEN the HEAVY class on the now-idle box (and serialized by the\r\n      # heavy-broker-pty test-group). The <HEAVY> filter MUST stay identical in both\r\n      # `-E` strings below AND in .config/nextest.toml's override — Phase A is the\r\n      # exact negation of Phase B, so the two phases partition the suite with nothing\r\n      # dropped or double-run.\r\n      - name: Test — Phase A (light pool, full parallel)\r\n        run: cargo nextest run --workspace --no-fail-fast -E 'not ( (package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|r…\n      - name: Test — Phase B (heavy class, serialized, on a now-quiet box)\r\n        run: cargo nextest run --workspace --no-fail-fast -E '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|run_no_…\n      # nextest does not run doctests — keep them on `cargo test --doc`.\r\n      - name: Doctests\r\n        run: cargo test --workspace --doc\r\n      - name: Clippy (deny warnings)\r\n        run: cargo clippy --workspace --all-targets -- -D warnings\r\n\r\n      # ---- M5-D8a real-shell dogfood E2E (thin cross-repo hook) ----\r\n…\n      # (only spt-core + the release channel moved orgs).","startLine":148,"lineNumbers":[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,null,191]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.github\\workflows\\ci.yml"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":529,"totalBytes":3847,"outputLines":35,"outputBytes":3847,"shownRange":{"start":149,"end":183},"nextOffset":184},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195791774}}
{"type":"message","id":"06ef134d","parentId":"3bd78bed","timestamp":"2026-07-16T09:56:31.782Z","message":{"role":"toolResult","toolCallId":"call_00UQSFeCXSEfa2s6MvHZVdhY|fc_0b42584a8ab0545d016a58aacfb8d481998a63551b6f146f76","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## applyhost.rs\n 852|\n*853|    fn echo_spawn_req() -> SpawnReq {\n 854|        #[cfg(unix)]\n 855|        let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 856|        #[cfg(windows)]\n...\n 884|        let mut seat = cold_connect_retry(&name);\n*885|        let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");\n 886|        let child_pid = broker.session_pid(sid);\n 887|        drop(seat); // the seat brain detaches; the broker keeps the child\n 888|\n...\n 1015|        let mut seat = cold_connect_retry(&name);\n*1016|        let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");\n 1017|        let child_pid = broker.session_pid(sid);\n 1018|        drop(seat);\n 1019|\n## attach.rs\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 59|/// [`mark_driven_by`]: a session served without a perch has no resting\n 60|/// record to move.\n...\n 139|/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\n*140|/// bounded channel is full (a controller that falls behind its own echo under a\n 141|/// hard flood). The next frame this reader sees is then a FORWARD gap that the\n 142|/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\n 143|/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\n...\n 421|                        // Input/Resize before Request, a viewer's input/resize,\n*422|                        // or target-direction records echoed back: noise — ignore.\n 423|                        _ => {}\n 424|                    }\n 425|                }\n## brain.rs\n 539|    /// the wire's `op_id`/`minter` fields are stamped together — an unstamped op is\n*540|    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\n 541|    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\n 542|        let sid = self.require_session()?;\n 543|        self.send(\n## brainproc.rs\n 418|pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n*419|    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n 420|}\n 421|\n 422|/// The supervisor's next backoff after a brain death: a healthy run (≥\n...\n 563|    fn ready_exe_hash(&self) -> Option<String> {\n*564|        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n 565|        parse_ready_exe_hash(&body)\n 566|    }\n 567|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n## broker.rs\n 306|/// a REAL fault (F029 C-1, REQ-TRANSLATE-COMMIT-MISS-TOLERANCE). A single missed\n*307|/// `{commit}` is **not** a fault — the stdin write SUCCEEDED, so the binary is\n 308|/// alive; the watchdog's job is ANTI-STALL (release the operator floor), not\n 309|/// execution-verification. Terminating a healthy binary over one eaten `{commit}`\n 310|/// was B6's exact root: the checkpoint clear boundary self-sends a FIRE-with-\n...\n 357|// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n*358|// byte (positive readiness probe, not a blind timer); (2) MUST echo-verify the head\n 359|// after typing → re-drive once → LOUD spool (silent+unrecoverable → recoverable+loud);\n 360|// (3) chunked-paced write under the gate (bracketed-paste deferred — the binary owns\n 361|// the key/text interleave, not a clean wrap seam; F-019 found it moot for CC submit).\n...\n 364|/// positive readiness signal before typing the first payload byte. Bounded SHORT so a\n*365|/// non-echoing PTY (ConPTY does not surface a programmatic probe's echo) adds only a\n 366|/// small one-time latency — the gate is a worker-local ONE-SHOT (it protects the first\n*367|/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n 368|/// returns in a tick or two, well under this.\n 369|const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n*370|/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n 371|const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372|/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373|/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n...\n 388|\n*389|/// The echo-verify wait (Layer 2): how long the worker waits after a `{commit}` for\n*390|/// the typed payload's head to echo back on the session ring before declaring a\n 391|/// verify MISS (round-trip + XON-resume latency bound).\n*392|const INJECT_ECHO_VERIFY_DEADLINE: Duration = Duration::from_millis(1200);\n 393|\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 399|            .map(Duration::from_millis)\n*400|            .unwrap_or(INJECT_ECHO_VERIFY_DEADLINE),\n*401|        Err(_) => INJECT_ECHO_VERIFY_DEADLINE,\n 402|    }\n 403|}\n 404|\n*405|/// How many bytes of the typed payload's HEAD we look for in the PTY echo. The bug\n 406|/// is head-loss specifically, so a leading-prefix match is the discriminating probe\n*407|/// (a swallowed head means the prefix is ABSENT even though a tail may echo).\n*408|const ECHO_VERIFY_PREFIX: usize = 24;\n 409|\n 410|/// Total delivery attempts per sequence: the first drive + ONE re-drive on an\n*411|/// echo-verify miss (doyle ruling A layer 2: \"re-drive once; on a second miss, LOUD\n 412|/// spool\"). 2 = one initial + one retry.\n 413|const INJECT_REDRIVE_ATTEMPTS: u32 = 2;\n 414|\n...\n 429|/// Does `haystack` contain `needle` as a contiguous subslice? (Small, allocation-free\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...\n 440|\n*441|/// Pure echo-verify decision (Layer 2, REQ-INJECT-MULTILINE-INTEGRITY): did the\n*442|/// typed payload's HEAD echo back? Compare a bounded prefix of `sent_text` against\n## config.rs\n 241|    pub fn load_from(path: &Path) -> Self {\n*242|        let Ok(raw) = std::fs::read_to_string(path) else {\n 243|            return Self::default();\n 244|        };\n 245|        let file: DaemonConfigFile = match serde_json::from_str(&raw) {\n## daemon.rs\n 691|        cmd.args([\"daemon\", \"run\"])\n*692|            .stdin(Stdio::null())\n 693|            .stdout(Stdio::null())\n 694|            .stderr(Stdio::null());\n 695|        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n...\n 1150|        .creation_flags(CREATE_NO_WINDOW)\n*1151|        .stdin(std::process::Stdio::null())\n 1152|        .output()?;\n 1153|    if !out.status.success() {\n 1154|        return Err(io::Error::other(format!(\n...\n 1200|            .creation_flags(CREATE_NO_WINDOW)\n*1201|            .stdin(std::process::Stdio::null())\n 1202|            .output()\n 1203|    };\n 1204|\n...\n 1606|            while Instant::now() < deadline {\n*1607|                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608|                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609|                        if p != 0 {\n 1610|                            found = Some(p);\n...\n 1641|        let diagpath = std::env::temp_dir().join(format!(\"spt_breakaway_diag_{}.log\", std::process::id()));\n*1642|        let diag = std::fs::read_to_string(&diagpath).unwrap_or_default();\n 1643|        let _ = std::fs::remove_file(&diagpath);\n 1644|\n 1645|        // Second skip path (sibling of the ACCESS_DENIED skip above): on some\n## deadline.rs\n 117|    fn load(path: &Path) -> Option<Self> {\n*118|        let raw = std::fs::read_to_string(path).ok()?;\n 119|        serde_json::from_str(&raw).ok()\n 120|    }\n 121|\n## deelevate.rs\n 197|    pub(super) fn elected_user_from(path: &std::path::Path) -> Option<SudoInvoker> {\n*198|        let name = std::fs::read_to_string(path).ok()?;\n 199|        user_by_name(name.trim())\n 200|    }\n 201|\n...\n 306|    /// One interactive line: \"Default user account for spt [<name>]: \".\n*307|    /// `None` when stdin is not a terminal or the user just pressed Enter.\n 308|    fn prompt_election(default_name: &str) -> Option<String> {\n 309|        use std::io::{IsTerminal, Write};\n*310|        if !std::io::stdin().is_terminal() {\n 311|            return None;\n 312|        }\n 313|        eprint!(\n...\n 318|        let mut s = String::new();\n*319|        std::io::stdin().read_line(&mut s).ok()?;\n 320|        let s = s.trim().to_string();\n 321|        (!s.is_empty()).then_some(s)\n 322|    }\n## digest.rs\n 8|//! on every output chunk. The digest is now a **projection of the same normalized\n*9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n 10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n 11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n 12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n...\n 417|fn log_less_activity(id: &str, perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*418|    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 419|        return Vec::new();\n 420|    };\n 421|    let mut items = Vec::new();\n...\n 462|fn read_context_items(perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*463|    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 464|        return Vec::new();\n 465|    };\n 466|    s.lines()\n## dispatch.rs\n 424|            // origin stays handshake-proven. The serve applies the event\n*425|            // through the LOCAL transition host — echo gate, wake effects,\n 426|            // and shell cascade run on this node, where the instance lives.\n 427|            // [impl->REQ-INST-6]\n 428|            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n...\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## docshost.rs\n 27|/// The default docs port (operator-ruled: 547 rejected — privileged + IANA\n*28|/// dhcpv6-server; 5474 is its unprivileged echo).\n 29|pub const DEFAULT_DOCS_PORT: u16 = 5474;\n 30|\n 31|/// The env override for the docs port (wins over `daemon.json`).\n## effect.rs\n 930|        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n*931|        let text = std::fs::read_to_string(j.path()).expect(\"read journal file\");\n 932|        // New line shape carries the minter tag: \"PENDING 1 cli 2 net-send\".\n 933|        let net_token = format!(\"{} {} {}\", net_key.class, net_key.minter.as_tag(), net_key.op);\n 934|        let pty_token = format!(\"{} {} {}\", pty_key.class, pty_key.minter.as_tag(), pty_key.op);\n## endpoint.rs\n 98|pub fn read_pump_heartbeat() -> Option<u64> {\n*99|    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n 100|    serde_json::from_str::<u64>(s.trim()).ok()\n 101|}\n## harnesshost.rs\n 40|/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\n*41|/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\n 42|pub const DEFAULT_HARNESS_ROWS: u16 = 24;\n 43|pub const DEFAULT_HARNESS_COLS: u16 = 80;\n 44|\n## lib.rs\n 249|pub use resting::{\n*250|    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend, fire_wake_effects,\n 251|    read_rest, request_freshness_pull, route_rest_event, take_freshness_pull, transition,\n 252|    write_rest, EdgeReport, RestEvent, RestRecord, RestRoute, RestState,\n 253|    NOT_A_HOSTED_PERCH_MARKER, PULL_MARKER_FILE,\n## lifecycle.rs\n 4|//! In the no-daemon interim model the long-lived `spt api listen` process hosted\n*5|//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo\n 6|//! fires, graceful signoff) directly in the agent's session. Consolidation\n 7|//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the\n 8|//! brain runs it as scheduled in-process loops, with a durable, configurable\n...\n 11|//! a re-host, not a rewrite: the heavy lifting stays in the already-pure\n*12|//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],\n 13|//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is\n 14|//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 — the\n 15|//! resident `spawn_psyche` seam retired); the ordering invariants\n*16|//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live\n 17|//! where they always have.\n 18|// [impl->REQ-DAEMON-1]\n 19|\n...\n 26|use spt_live::{\n*27|    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n 28|    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n 29|};\n 30|use spt_runtime::{Manifest, ManifestRuntime};\n...\n 41|\n*42|/// Bound for a bounded history/echo invocation in the host (carried from M2b).\n 43|const LIVE_BOUNDED_TIMEOUT: Duration = Duration::from_secs(30);\n 44|\n 45|/// The per-event psyche turn's timeout FLOOR (F-030 design §7 Q1): a pulse period\n...\n 161|/// context-file), LOCAL to the turn. `base` is `base_keys(parent_sid)` — its\n*162|/// `{session_id}` is the PARENT's (echo/history/signoff keep that fill); here we\n 163|/// OVERRIDE `{session_id}` to the psyche's OWN sid and add the parent's sid under its\n 164|/// OWN explicit key `{parent_session_id}` (never aliased). `context_file` is the PATH\n 165|/// to the core-written mind file (F-030 W4, REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): the\n...\n 262|\n*263|/// Loud-ONCE-per-endpoint diagnostic when the ECHO fire skips because a RELATIVE\n*264|/// `commune_dir` has no endpoint cwd to resolve against (REQ-ECHO-DROP-DIR-RESOLVE):\n 265|/// the skip is NAMED, never a raw relative write into whatever cwd the daemon\n 266|/// happens to hold (System32 under WMI = os error 5; a writable daemon cwd =\n 267|/// communes silently lost to a wrong dir — the latent-worse variant).\n*268|// [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n*269|fn warn_echo_no_cwd_once(id: &str, raw: &Path) {\n 270|    warn_once(\n*271|        format!(\"echo:{id}\"),\n 272|        format!(\n*273|            \"ECHO_COMMUNE_SKIP:{id}: relative commune dir {raw:?} has no endpoint cwd to \\\n*274|             resolve against — echo commune skipped until bind records a cwd\"\n 275|        ),\n 276|    );\n 277|}\n...\n 279|/// Quiet-ONCE-per-endpoint note when commune-sync skips because the manifest\n*280|/// declares no `[session.echo_commune]` role (REQ-PSYCHE-ROLE-OPTIONAL-SKIP):\n 281|/// the role is an OPTIONAL template per the published contract — its absence is\n 282|/// a configuration fact, never a turn failure, never a strike.\n 283|// [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n*284|fn note_echo_role_absent_once(id: &str) {\n 285|    warn_once(\n*286|        format!(\"echo-role:{id}\"),\n 287|        format!(\n*288|            \"ECHO_ROLE_ABSENT:{id}: manifest declares no [session.echo_commune] role — \\\n 289|             commune-sync skipped (the role is optional)\"\n 290|        ),\n 291|    );\n...\n 293|\n*294|/// One pulse tick's observed effects: any drops ingested + whether an echo fired.\n 295|#[derive(Debug, Clone, Default)]\n 296|pub struct TickReport {\n 297|    pub ingested: Vec<Ingested>,\n*298|    pub echo_fired: bool,\n*299|    /// F-030 W1: the per-event psyche-turn outcome when the echo-gate fired this\n 300|    /// tick. `None` ⇒ no fire this tick; `Some(Ok(()))` ⇒ the turn AND its commune-\n 301|    /// sync side-effect both succeeded; `Some(Err(reason))` ⇒ the turn OR the\n 302|    /// commune-sync failed (a strike — [`run_pulse_loop`] feeds it the failure budget).\n## linkhost.rs\n 471|                    else {\n*472|                        continue; // echoed replies / protocol noise: degrade\n 473|                    };\n 474|                    // Unsolicited wire inbound — the ADR-0009 gate under the\n 475|                    // transport-proven origin. Deny = finish, no reply.\n...\n 545|                    // instance relaunches so the spooled command actually\n*546|                    // drains; stdin receipt pushes pending now, best-effort.\n 547|                    let mut detail = format!(\"spooled op={op} at {id}\");\n 548|                    let offline = shellinfo::resolve_shell_ref(owlery, owner, &id)\n 549|                        .map(|(_, i)| i.status != SHELL_STATUS_ONLINE)\n...\n 557|                    }\n*558|                    if shell.command_receipt == Some(CommandReceipt::Stdin) {\n*559|                        match shellchan::deliver_stdin_pending(owlery, owner, &id) {\n*560|                            Ok(n) => detail.push_str(&format!(\"; {n} frame(s) via stdin\")),\n*561|                            Err(e) => detail.push_str(&format!(\"; stdin pending: {e}\")),\n 562|                        }\n 563|                    }\n 564|                    (\n...\n 693|                    let ShellLinkRecord::Reply { outcome, detail } = rec else {\n*694|                        continue; // our own request echoed: noise\n 695|                    };\n 696|                    return Ok(match outcome.as_str() {\n 697|                        \"ok\" => ShellLinkRequestOutcome::Ok(detail),\n## livehost.rs\n 297|fn read_resume_pid(perch: &Path) -> Option<u32> {\n*298|    std::fs::read_to_string(perch.join(RESUME_PID_FILE))\n 299|        .ok()\n 300|        .and_then(|s| s.trim().parse::<u32>().ok())\n 301|        .filter(|&p| p != 0)\n\n## bin/\n### xlate_choreo_fixture.rs\n 1|//! W2 Layer G integration fixture: a REAL ADR-0022 translation binary —\n*2|//! a pure stdin→stdout JSON-lines filter spt-core lifecycle-manages. Used by\n 3|//! `tests/inject_control_wedge.rs` (referenced via\n 4|//! `env!(\"CARGO_BIN_EXE_xlate_choreo_fixture\")`). NOT a production binary.\n 5|//!\n*6|//! Protocol (spt-core → here, one JSON object per line on stdin):\n 7|//!   `{\"type\":\"init\",…}`  — identity; noted, never drives output.\n 8|//!   `{\"type\":\"input\"}`   — a CONTENT-FREE operator-activity ping; ignored.\n 9|//!   `{\"type\":\"event\",\"envelope\":\"…\"}` — an inbound `<EVENT>`; we respond with\n...\n 30|//!\n*31|//! Privacy assertion support (doyle gate #1): every stdin line we receive is\n*32|//! appended verbatim to the file named by env `XLATE_FIXTURE_STDIN_LOG` (when\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...\n 44|    let mode = std::env::var(\"XLATE_FIXTURE_MODE\").unwrap_or_else(|_| \"choreo\".to_string());\n*45|    let stdin_log = std::env::var(\"XLATE_FIXTURE_STDIN_LOG\").ok();\n 46|\n*47|    let stdin = io::stdin();\n 48|    let mut out = io::stdout();\n 49|\n 50|    // `nocommit_once` misses only the FIRST event, then commits — count events seen.\n...\n 52|\n*53|    for line in stdin.lock().lines() {\n*54|        let Ok(line) = line else { break }; // stdin closed (EOF) → exit (no-zombie).\n 55|        let trimmed = line.trim();\n 56|        if trimmed.is_empty() {\n 57|            continue;\n...\n 59|\n*60|        // Record EVERY received stdin line for the privacy (content-free ping)\n 61|        // assertion. Best-effort: a log failure never changes our behavior.\n*62|        if let Some(path) = &stdin_log {\n 63|            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n 64|                let _ = writeln!(f, \"{trimmed}\");\n 65|            }\n...\n 107|            // `ctrl+s` (XOFF, 0x13) emitted above PAUSED terminal output. On a\n*108|            // forkpty PTY (IXON honored) that pause STRANDS every subsequent echo —\n 109|            // the deadline-flushed operator keystrokes (op_flushed), observed via the\n*110|            // child's echo, never reach the output ring → the g2 op_flushed read\n 111|            // times out (forkpty-ONLY; Windows ConPTY ignores XOFF, so g2 passed\n 112|            // there and the strand stayed hidden until the forkpty matrix). The happy path already\n 113|            // emits this XON before `{commit}`; an adapter that ABORTS mid-sequence\n...\n 122|        // XON (`ctrl+q` = 0x11): resume the terminal output flow the `ctrl+s` (XOFF)\n*123|        // chord paused, so the payload's echo flushes deterministically on a ConPTY\n*124|        // (the int test observes byte-receipt via that echo). A real stash/restore\n 125|        // adapter restores flow at the end of its sequence just the same; this keeps\n 126|        // the `ctrl+s` chord (and its real 0x13 mapping + floor exercise) intact while\n 127|        // making the payload's arrival observable, per the test-specialist's latitude.\n\nShowing files 1-20 of 204+. Use skip=20 for the next page, or narrow paths/pattern.\n\nSkipped missing paths: crates/mock-adapter"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":93,"fileCount":20,"files":["crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.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/config.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/linkhost.rs","crates/spt-daemon/src/livehost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":3},{"path":"crates/spt-daemon/src/attach.rs","count":3},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":14},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-daemon/src/brainproc.rs","count":2},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/config.rs","count":1},{"path":"crates/spt-daemon/src/daemon.rs","count":5},{"path":"crates/spt-daemon/src/deadline.rs","count":1},{"path":"crates/spt-daemon/src/deelevate.rs","count":4},{"path":"crates/spt-daemon/src/digest.rs","count":3},{"path":"crates/spt-daemon/src/dispatch.rs","count":3},{"path":"crates/spt-daemon/src/docshost.rs","count":1},{"path":"crates/spt-daemon/src/effect.rs","count":1},{"path":"crates/spt-daemon/src/endpoint.rs","count":1},{"path":"crates/spt-daemon/src/harnesshost.rs","count":1},{"path":"crates/spt-daemon/src/lib.rs","count":1},{"path":"crates/spt-daemon/src/lifecycle.rs","count":20},{"path":"crates/spt-daemon/src/linkhost.rs","count":7},{"path":"crates/spt-daemon/src/livehost.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## applyhost.rs\n  852│\n *853│    fn echo_spawn_req() -> SpawnReq {\n  854│        #[cfg(unix)]\n  855│        let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n  856│        #[cfg(windows)]\n     │...\n  884│        let mut seat = cold_connect_retry(&name);\n *885│        let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");\n  886│        let child_pid = broker.session_pid(sid);\n  887│        drop(seat); // the seat brain detaches; the broker keeps the child\n  888│\n     │...\n 1015│        let mut seat = cold_connect_retry(&name);\n*1016│        let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");\n 1017│        let child_pid = broker.session_pid(sid);\n 1018│        drop(seat);\n 1019│\n## attach.rs\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  59│/// [`mark_driven_by`]: a session served without a perch has no resting\n  60│/// record to move.\n    │...\n 139│/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\n*140│/// bounded channel is full (a controller that falls behind its own echo under a\n 141│/// hard flood). The next frame this reader sees is then a FORWARD gap that the\n 142│/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\n 143│/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\n    │...\n 421│                        // Input/Resize before Request, a viewer's input/resize,\n*422│                        // or target-direction records echoed back: noise — ignore.\n 423│                        _ => {}\n 424│                    }\n 425│                }\n## brain.rs\n 539│    /// the wire's `op_id`/`minter` fields are stamped together — an unstamped op is\n*540│    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\n 541│    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\n 542│        let sid = self.require_session()?;\n 543│        self.send(\n## brainproc.rs\n 418│pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n*419│    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n 420│}\n 421│\n 422│/// The supervisor's next backoff after a brain death: a healthy run (≥\n    │...\n 563│    fn ready_exe_hash(&self) -> Option<String> {\n*564│        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n 565│        parse_ready_exe_hash(&body)\n 566│    }\n 567│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n## broker.rs\n 306│/// a REAL fault (F029 C-1, REQ-TRANSLATE-COMMIT-MISS-TOLERANCE). A single missed\n*307│/// `{commit}` is **not** a fault — the stdin write SUCCEEDED, so the binary is\n 308│/// alive; the watchdog's job is ANTI-STALL (release the operator floor), not\n 309│/// execution-verification. Terminating a healthy binary over one eaten `{commit}`\n 310│/// was B6's exact root: the checkpoint clear boundary self-sends a FIRE-with-\n    │...\n 357│// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n*358│// byte (positive readiness probe, not a blind timer); (2) MUST echo-verify the head\n 359│// after typing → re-drive once → LOUD spool (silent+unrecoverable → recoverable+loud);\n 360│// (3) chunked-paced write under the gate (bracketed-paste deferred — the binary owns\n 361│// the key/text interleave, not a clean wrap seam; F-019 found it moot for CC submit).\n    │...\n 364│/// positive readiness signal before typing the first payload byte. Bounded SHORT so a\n*365│/// non-echoing PTY (ConPTY does not surface a programmatic probe's echo) adds only a\n 366│/// small one-time latency — the gate is a worker-local ONE-SHOT (it protects the first\n*367│/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n 368│/// returns in a tick or two, well under this.\n 369│const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n*370│/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n 371│const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372│/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373│/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n    │...\n 388│\n*389│/// The echo-verify wait (Layer 2): how long the worker waits after a `{commit}` for\n*390│/// the typed payload's head to echo back on the session ring before declaring a\n 391│/// verify MISS (round-trip + XON-resume latency bound).\n*392│const INJECT_ECHO_VERIFY_DEADLINE: Duration = Duration::from_millis(1200);\n 393│\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 399│            .map(Duration::from_millis)\n*400│            .unwrap_or(INJECT_ECHO_VERIFY_DEADLINE),\n*401│        Err(_) => INJECT_ECHO_VERIFY_DEADLINE,\n 402│    }\n 403│}\n 404│\n*405│/// How many bytes of the typed payload's HEAD we look for in the PTY echo. The bug\n 406│/// is head-loss specifically, so a leading-prefix match is the discriminating probe\n*407│/// (a swallowed head means the prefix is ABSENT even though a tail may echo).\n*408│const ECHO_VERIFY_PREFIX: usize = 24;\n 409│\n 410│/// Total delivery attempts per sequence: the first drive + ONE re-drive on an\n*411│/// echo-verify miss (doyle ruling A layer 2: \"re-drive once; on a second miss, LOUD\n 412│/// spool\"). 2 = one initial + one retry.\n 413│const INJECT_REDRIVE_ATTEMPTS: u32 = 2;\n 414│\n    │...\n 429│/// Does `haystack` contain `needle` as a contiguous subslice? (Small, allocation-free\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    │...\n 440│\n*441│/// Pure echo-verify decision (Layer 2, REQ-INJECT-MULTILINE-INTEGRITY): did the\n*442│/// typed payload's HEAD echo back? Compare a bounded prefix of `sent_text` against\n## config.rs\n 241│    pub fn load_from(path: &Path) -> Self {\n*242│        let Ok(raw) = std::fs::read_to_string(path) else {\n 243│            return Self::default();\n 244│        };\n 245│        let file: DaemonConfigFile = match serde_json::from_str(&raw) {\n## daemon.rs\n  691│        cmd.args([\"daemon\", \"run\"])\n *692│            .stdin(Stdio::null())\n  693│            .stdout(Stdio::null())\n  694│            .stderr(Stdio::null());\n  695│        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n     │...\n 1150│        .creation_flags(CREATE_NO_WINDOW)\n*1151│        .stdin(std::process::Stdio::null())\n 1152│        .output()?;\n 1153│    if !out.status.success() {\n 1154│        return Err(io::Error::other(format!(\n     │...\n 1200│            .creation_flags(CREATE_NO_WINDOW)\n*1201│            .stdin(std::process::Stdio::null())\n 1202│            .output()\n 1203│    };\n 1204│\n     │...\n 1606│            while Instant::now() < deadline {\n*1607│                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608│                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609│                        if p != 0 {\n 1610│                            found = Some(p);\n     │...\n 1641│        let diagpath = std::env::temp_dir().join(format!(\"spt_breakaway_diag_{}.log\", std::process::id()));\n*1642│        let diag = std::fs::read_to_string(&diagpath).unwrap_or_default();\n 1643│        let _ = std::fs::remove_file(&diagpath);\n 1644│\n 1645│        // Second skip path (sibling of the ACCESS_DENIED skip above): on some\n## deadline.rs\n 117│    fn load(path: &Path) -> Option<Self> {\n*118│        let raw = std::fs::read_to_string(path).ok()?;\n 119│        serde_json::from_str(&raw).ok()\n 120│    }\n 121│\n## deelevate.rs\n 197│    pub(super) fn elected_user_from(path: &std::path::Path) -> Option<SudoInvoker> {\n*198│        let name = std::fs::read_to_string(path).ok()?;\n 199│        user_by_name(name.trim())\n 200│    }\n 201│\n    │...\n 306│    /// One interactive line: \"Default user account for spt [<name>]: \".\n*307│    /// `None` when stdin is not a terminal or the user just pressed Enter.\n 308│    fn prompt_election(default_name: &str) -> Option<String> {\n 309│        use std::io::{IsTerminal, Write};\n*310│        if !std::io::stdin().is_terminal() {\n 311│            return None;\n 312│        }\n 313│        eprint!(\n    │...\n 318│        let mut s = String::new();\n*319│        std::io::stdin().read_line(&mut s).ok()?;\n 320│        let s = s.trim().to_string();\n 321│        (!s.is_empty()).then_some(s)\n 322│    }\n## digest.rs\n   8│//! on every output chunk. The digest is now a **projection of the same normalized\n  *9│//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n  10│//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n  11│//! records via [`project_endpoint_digest`], and the hub publishes the result.\n  12│//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n    │...\n 417│fn log_less_activity(id: &str, perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*418│    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 419│        return Vec::new();\n 420│    };\n 421│    let mut items = Vec::new();\n    │...\n 462│fn read_context_items(perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*463│    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 464│        return Vec::new();\n 465│    };\n 466│    s.lines()\n## dispatch.rs\n 424│            // origin stays handshake-proven. The serve applies the event\n*425│            // through the LOCAL transition host — echo gate, wake effects,\n 426│            // and shell cascade run on this node, where the instance lives.\n 427│            // [impl->REQ-INST-6]\n 428│            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n    │...\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## docshost.rs\n 27│/// The default docs port (operator-ruled: 547 rejected — privileged + IANA\n*28│/// dhcpv6-server; 5474 is its unprivileged echo).\n 29│pub const DEFAULT_DOCS_PORT: u16 = 5474;\n 30│\n 31│/// The env override for the docs port (wins over `daemon.json`).\n## effect.rs\n 930│        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n*931│        let text = std::fs::read_to_string(j.path()).expect(\"read journal file\");\n 932│        // New line shape carries the minter tag: \"PENDING 1 cli 2 net-send\".\n 933│        let net_token = format!(\"{} {} {}\", net_key.class, net_key.minter.as_tag(), net_key.op);\n 934│        let pty_token = format!(\"{} {} {}\", pty_key.class, pty_key.minter.as_tag(), pty_key.op);\n## endpoint.rs\n  98│pub fn read_pump_heartbeat() -> Option<u64> {\n *99│    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n 100│    serde_json::from_str::<u64>(s.trim()).ok()\n 101│}\n## harnesshost.rs\n 40│/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\n*41│/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\n 42│pub const DEFAULT_HARNESS_ROWS: u16 = 24;\n 43│pub const DEFAULT_HARNESS_COLS: u16 = 80;\n 44│\n## lib.rs\n 249│pub use resting::{\n*250│    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend, fire_wake_effects,\n 251│    read_rest, request_freshness_pull, route_rest_event, take_freshness_pull, transition,\n 252│    write_rest, EdgeReport, RestEvent, RestRecord, RestRoute, RestState,\n 253│    NOT_A_HOSTED_PERCH_MARKER, PULL_MARKER_FILE,\n## lifecycle.rs\n   4│//! In the no-daemon interim model the long-lived `spt api listen` process hosted\n  *5│//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo\n   6│//! fires, graceful signoff) directly in the agent's session. Consolidation\n   7│//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the\n   8│//! brain runs it as scheduled in-process loops, with a durable, configurable\n    │...\n  11│//! a re-host, not a rewrite: the heavy lifting stays in the already-pure\n *12│//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],\n  13│//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is\n  14│//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 — the\n  15│//! resident `spawn_psyche` seam retired); the ordering invariants\n *16│//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live\n  17│//! where they always have.\n  18│// [impl->REQ-DAEMON-1]\n  19│\n    │...\n  26│use spt_live::{\n *27│    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n  28│    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n  29│};\n  30│use spt_runtime::{Manifest, ManifestRuntime};\n    │...\n  41│\n *42│/// Bound for a bounded history/echo invocation in the host (carried from M2b).\n  43│const LIVE_BOUNDED_TIMEOUT: Duration = Duration::from_secs(30);\n  44│\n  45│/// The per-event psyche turn's timeout FLOOR (F-030 design §7 Q1): a pulse period\n    │...\n 161│/// context-file), LOCAL to the turn. `base` is `base_keys(parent_sid)` — its\n*162│/// `{session_id}` is the PARENT's (echo/history/signoff keep that fill); here we\n 163│/// OVERRIDE `{session_id}` to the psyche's OWN sid and add the parent's sid under its\n 164│/// OWN explicit key `{parent_session_id}` (never aliased). `context_file` is the PATH\n 165│/// to the core-written mind file (F-030 W4, REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): the\n    │...\n 262│\n*263│/// Loud-ONCE-per-endpoint diagnostic when the ECHO fire skips because a RELATIVE\n*264│/// `commune_dir` has no endpoint cwd to resolve against (REQ-ECHO-DROP-DIR-RESOLVE):\n 265│/// the skip is NAMED, never a raw relative write into whatever cwd the daemon\n 266│/// happens to hold (System32 under WMI = os error 5; a writable daemon cwd =\n 267│/// communes silently lost to a wrong dir — the latent-worse variant).\n*268│// [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n*269│fn warn_echo_no_cwd_once(id: &str, raw: &Path) {\n 270│    warn_once(\n*271│        format!(\"echo:{id}\"),\n 272│        format!(\n*273│            \"ECHO_COMMUNE_SKIP:{id}: relative commune dir {raw:?} has no endpoint cwd to \\\n*274│             resolve against — echo commune skipped until bind records a cwd\"\n 275│        ),\n 276│    );\n 277│}\n    │...\n 279│/// Quiet-ONCE-per-endpoint note when commune-sync skips because the manifest\n*280│/// declares no `[session.echo_commune]` role (REQ-PSYCHE-ROLE-OPTIONAL-SKIP):\n 281│/// the role is an OPTIONAL template per the published contract — its absence is\n 282│/// a configuration fact, never a turn failure, never a strike.\n 283│// [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n*284│fn note_echo_role_absent_once(id: &str) {\n 285│    warn_once(\n*286│        format!(\"echo-role:{id}\"),\n 287│        format!(\n*288│            \"ECHO_ROLE_ABSENT:{id}: manifest declares no [session.echo_commune] role — \\\n 289│             commune-sync skipped (the role is optional)\"\n 290│        ),\n 291│    );\n    │...\n 293│\n*294│/// One pulse tick's observed effects: any drops ingested + whether an echo fired.\n 295│#[derive(Debug, Clone, Default)]\n 296│pub struct TickReport {\n 297│    pub ingested: Vec<Ingested>,\n*298│    pub echo_fired: bool,\n*299│    /// F-030 W1: the per-event psyche-turn outcome when the echo-gate fired this\n 300│    /// tick. `None` ⇒ no fire this tick; `Some(Ok(()))` ⇒ the turn AND its commune-\n 301│    /// sync side-effect both succeeded; `Some(Err(reason))` ⇒ the turn OR the\n 302│    /// commune-sync failed (a strike — [`run_pulse_loop`] feeds it the failure budget).\n## linkhost.rs\n 471│                    else {\n*472│                        continue; // echoed replies / protocol noise: degrade\n 473│                    };\n 474│                    // Unsolicited wire inbound — the ADR-0009 gate under the\n 475│                    // transport-proven origin. Deny = finish, no reply.\n    │...\n 545│                    // instance relaunches so the spooled command actually\n*546│                    // drains; stdin receipt pushes pending now, best-effort.\n 547│                    let mut detail = format!(\"spooled op={op} at {id}\");\n 548│                    let offline = shellinfo::resolve_shell_ref(owlery, owner, &id)\n 549│                        .map(|(_, i)| i.status != SHELL_STATUS_ONLINE)\n    │...\n 557│                    }\n*558│                    if shell.command_receipt == Some(CommandReceipt::Stdin) {\n*559│                        match shellchan::deliver_stdin_pending(owlery, owner, &id) {\n*560│                            Ok(n) => detail.push_str(&format!(\"; {n} frame(s) via stdin\")),\n*561│                            Err(e) => detail.push_str(&format!(\"; stdin pending: {e}\")),\n 562│                        }\n 563│                    }\n 564│                    (\n    │...\n 693│                    let ShellLinkRecord::Reply { outcome, detail } = rec else {\n*694│                        continue; // our own request echoed: noise\n 695│                    };\n 696│                    return Ok(match outcome.as_str() {\n 697│                        \"ok\" => ShellLinkRequestOutcome::Ok(detail),\n## livehost.rs\n 297│fn read_resume_pid(perch: &Path) -> Option<u32> {\n*298│    std::fs::read_to_string(perch.join(RESUME_PID_FILE))\n 299│        .ok()\n 300│        .and_then(|s| s.trim().parse::<u32>().ok())\n 301│        .filter(|&p| p != 0)\n\n## bin/\n### xlate_choreo_fixture.rs\n   1│//! W2 Layer G integration fixture: a REAL ADR-0022 translation binary —\n  *2│//! a pure stdin→stdout JSON-lines filter spt-core lifecycle-manages. Used by\n   3│//! `tests/inject_control_wedge.rs` (referenced via\n   4│//! `env!(\"CARGO_BIN_EXE_xlate_choreo_fixture\")`). NOT a production binary.\n   5│//!\n  *6│//! Protocol (spt-core → here, one JSON object per line on stdin):\n   7│//!   `{\"type\":\"init\",…}`  — identity; noted, never drives output.\n   8│//!   `{\"type\":\"input\"}`   — a CONTENT-FREE operator-activity ping; ignored.\n   9│//!   `{\"type\":\"event\",\"envelope\":\"…\"}` — an inbound `<EVENT>`; we respond with\n    │...\n  30│//!\n *31│//! Privacy assertion support (doyle gate #1): every stdin line we receive is\n *32│//! appended verbatim to the file named by env `XLATE_FIXTURE_STDIN_LOG` (when\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    │...\n  44│    let mode = std::env::var(\"XLATE_FIXTURE_MODE\").unwrap_or_else(|_| \"choreo\".to_string());\n *45│    let stdin_log = std::env::var(\"XLATE_FIXTURE_STDIN_LOG\").ok();\n  46│\n *47│    let stdin = io::stdin();\n  48│    let mut out = io::stdout();\n  49│\n  50│    // `nocommit_once` misses only the FIRST event, then commits — count events seen.\n    │...\n  52│\n *53│    for line in stdin.lock().lines() {\n *54│        let Ok(line) = line else { break }; // stdin closed (EOF) → exit (no-zombie).\n  55│        let trimmed = line.trim();\n  56│        if trimmed.is_empty() {\n  57│            continue;\n    │...\n  59│\n *60│        // Record EVERY received stdin line for the privacy (content-free ping)\n  61│        // assertion. Best-effort: a log failure never changes our behavior.\n *62│        if let Some(path) = &stdin_log {\n  63│            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n  64│                let _ = writeln!(f, \"{trimmed}\");\n  65│            }\n    │...\n 107│            // `ctrl+s` (XOFF, 0x13) emitted above PAUSED terminal output. On a\n*108│            // forkpty PTY (IXON honored) that pause STRANDS every subsequent echo —\n 109│            // the deadline-flushed operator keystrokes (op_flushed), observed via the\n*110│            // child's echo, never reach the output ring → the g2 op_flushed read\n 111│            // times out (forkpty-ONLY; Windows ConPTY ignores XOFF, so g2 passed\n 112│            // there and the strand stayed hidden until the forkpty matrix). The happy path already\n 113│            // emits this XON before `{commit}`; an adapter that ABORTS mid-sequence\n    │...\n 122│        // XON (`ctrl+q` = 0x11): resume the terminal output flow the `ctrl+s` (XOFF)\n*123│        // chord paused, so the payload's echo flushes deterministically on a ConPTY\n*124│        // (the int test observes byte-receipt via that echo). A real stash/restore\n 125│        // adapter restores flow at the end of its sequence just the same; this keeps\n 126│        // the `ctrl+s` chord (and its real 0x13 mapping + floor exercise) intact while\n 127│        // making the payload's arrival observable, per the test-specialist's latitude.","missingPaths":["crates/mock-adapter"]},"isError":false,"timestamp":1784195791782}}
{"type":"message","id":"40e574c4","parentId":"06ef134d","timestamp":"2026-07-16T09:56:31.787Z","message":{"role":"toolResult","toolCallId":"call_UiIBCViJs6REnjprUOAhXRZz|fc_0b42584a8ab0545d016a58aacfb8dc8199824f46a8e1302074","toolName":"grep","content":[{"type":"text","text":"# crates/\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 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 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 809|        // Wire the supervisor signal; the verb now raises it and reports honored.\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 828|    #[test]\n*829|    fn refresh_brain_cycles_without_staging_or_swap_preconditions() {\n 830|        let name = unique_name();\n 831|        let broker = served_broker(&name);\n 832|\n...\n 834|        assert!(\n*835|            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n 836|            \"no supervisor wired must report honored=false\"\n 837|        );\n 838|\n...\n 840|        // apply path rides — with nothing staged anywhere.\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 846|        );\n 847|        assert!(\n#### brain.rs\n 37|use crate::msg::{\n*38|    decode_bytes, encode_bytes, AdapterApplyReq, AppliedEvent, BrainRestarted, BrokerImageReply, StallEvictsReply, DisplacedEvent,\n 39|    EndpointInjected, EndpointInputReq, ErrorEvent, ExitEvent,\n 40|    InputReq,\n 41|    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\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 1467|                }\n#### brainproc.rs\n 114|#[derive(Default)]\n*115|pub struct BrainRestart {\n 116|    requested: AtomicBool,\n 117|}\n 118|\n*119|impl BrainRestart {\n 120|    /// A fresh signal with no pending request.\n 121|    pub fn new() -> Self {\n 122|        Self::default()\n...\n 750|    stop: &AtomicBool,\n*751|    restart: &BrainRestart,\n 752|    base: Duration,\n 753|    env: &dyn TrialEnv,\n 754|    window: Duration,\n...\n 1091|/// seed-control loop. The brain child is supervised for the broker's lifetime.\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...\n 1098|    broker: Option<Arc<crate::broker::Broker>>,\n*1099|) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n*1100|    let restart = Arc::new(BrainRestart::new());\n 1101|    let restart_sup = Arc::clone(&restart);\n 1102|    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n 1103|    // start, before any `apply` can rename it under us. Linux current_exe()\n...\n 1183|        // Tiny base backoff so the test is quick; real production uses seconds.\n*1184|        let restart = BrainRestart::new();\n 1185|        supervise_brain(\n 1186|            &stop,\n 1187|            &restart,\n...\n 1204|\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...\n 1214|        let stop = Arc::new(AtomicBool::new(false));\n*1215|        let restart = Arc::new(BrainRestart::new());\n 1216|        let stop_c = Arc::clone(&stop);\n 1217|        let restart_c = Arc::clone(&restart);\n 1218|        let seen = Arc::new(std::sync::Mutex::new(Vec::<(u64, StartReason)>::new()));\n...\n 1258|        let seen_c = Arc::clone(&seen);\n*1259|        let restart = BrainRestart::new();\n 1260|        supervise_brain(\n 1261|            &stop,\n 1262|            &restart,\n...\n 1586|        let stop = Arc::new(AtomicBool::new(false));\n*1587|        let restart = BrainRestart::new();\n 1588|        let spawns = Arc::new(Mutex::new(Vec::<(u64, StartReason, Option<String>)>::new()));\n 1589|        let env_c = Arc::clone(&env);\n 1590|        let spawns_c = Arc::clone(&spawns);\n...\n 1640|        let stop = Arc::new(AtomicBool::new(false));\n*1641|        let restart = BrainRestart::new();\n 1642|        let env_c = Arc::clone(&env);\n 1643|        let stop_c = Arc::clone(&stop);\n 1644|        supervise_brain(\n...\n 1682|        let stop = Arc::new(AtomicBool::new(false));\n*1683|        let restart = BrainRestart::new();\n 1684|        let env_c = Arc::clone(&env);\n 1685|        let stop_c = Arc::clone(&stop);\n 1686|        let spawns = Arc::new(AtomicU32::new(0));\n...\n 1769|        let stop = Arc::new(AtomicBool::new(false));\n*1770|        let restart = BrainRestart::new();\n 1771|        let env_c = Arc::clone(&env);\n 1772|        let stop_c = Arc::clone(&stop);\n 1773|        let spawns = Arc::new(AtomicU32::new(0));\n...\n 1812|        let stop = Arc::new(AtomicBool::new(false));\n*1813|        let restart = BrainRestart::new();\n 1814|        let env_c = Arc::clone(&env);\n 1815|        let stop_c = Arc::clone(&stop);\n 1816|        let spawns = Arc::new(AtomicU32::new(0));\n...\n 1854|        let stop = Arc::new(AtomicBool::new(false));\n*1855|        let restart = BrainRestart::new();\n 1856|        let spawns = Arc::new(Mutex::new(Vec::<(u64, StartReason, Option<String>)>::new()));\n 1857|        let spawns_c = Arc::clone(&spawns);\n 1858|        let stop_c = Arc::clone(&stop);\n...\n 1909|        let stop = Arc::new(AtomicBool::new(false));\n*1910|        let restart = BrainRestart::new();\n 1911|        let candidate_pid = Arc::new(AtomicU32::new(0));\n 1912|        let cand_c = Arc::clone(&candidate_pid);\n 1913|        let stop_c = Arc::clone(&stop);\n...\n 1955|        let stop = Arc::new(AtomicBool::new(false));\n*1956|        let restart = BrainRestart::new();\n 1957|        let env_c = Arc::clone(&env);\n 1958|        let stop_c = Arc::clone(&stop);\n 1959|        supervise_brain(\n...\n 2003|        let stop = Arc::new(AtomicBool::new(false));\n*2004|        let restart = BrainRestart::new();\n 2005|        let stop_c = Arc::clone(&stop);\n 2006|        supervise_brain(\n 2007|            &stop,\n...\n 2086|        let stop = Arc::new(AtomicBool::new(false));\n*2087|        let restart = BrainRestart::new();\n 2088|        let spawns = Arc::new(Mutex::new(Vec::<Option<String>>::new()));\n 2089|        let spawns_c = Arc::clone(&spawns);\n 2090|        let stop_c = Arc::clone(&stop);\n#### broker.rs\n 55|    evicted_envelope, net_dialed_envelope, output_envelope,\n*56|    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n 57|    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n 58|    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n 59|    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n...\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 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...\n 3894|            KIND_BRAIN_RESTARTED,\n*3895|            serde_json::to_value(BrainRestarted { honored })\n*3896|                .expect(\"BrainRestarted serializes\"),\n 3897|        );\n 3898|        send_frame(send, &frame);\n 3899|    }\n#### daemon.rs\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#### 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 876|#[derive(Debug, Clone, Serialize, Deserialize)]\n*877|pub struct BrainRestarted {\n 878|    #[serde(default)]\n 879|    pub honored: bool,\n 880|}\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#### false_promote.rs\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 298|    let sup = thread::spawn(move || {\n*299|        let restart = BrainRestart::new();\n 300|        supervise_brain(\n 301|            &stop_sup,\n 302|            &restart,\n\n## spt/\n\n### src/\n#### cli.rs\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 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\n### tests/\n#### brain_survive.rs\n 68|use spt_daemon::brain::Brain;\n*69|use spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};\n 70|use spt_daemon::codec::write_frame;\n 71|use spt_daemon::endpoint::broker_socket_name;\n 72|use spt_daemon::frame::{Envelope, Role};\n...\n 257|    let stop = Arc::new(AtomicBool::new(false));\n*258|    let restart = Arc::new(BrainRestart::new());\n 259|    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260|    let home_path = home.path().to_path_buf();\n 261|\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...\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#### resume_no_control_steal_e2e.rs\n 97|\n*98|use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n 99|use spt_daemon::codec::{read_frame, write_frame};\n 100|use spt_daemon::endpoint::broker_socket_name;\n 101|use spt_daemon::frame::{Envelope, Role};\n...\n 399|        thread::spawn(move || {\n*400|            let restart = BrainRestart::new();\n 401|            supervise_brain(\n 402|                &stop_sup,\n 403|                &restart,\n#### trial_drain_drive_e2e.rs\n 108|use spt_daemon::brain::Brain;\n*109|use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n 110|use spt_daemon::codec::{read_frame, write_frame};\n 111|use spt_daemon::endpoint::broker_socket_name;\n 112|use spt_daemon::frame::{Envelope, Role};\n...\n 373|        thread::spawn(move || {\n*374|            let restart = BrainRestart::new();\n 375|            supervise_brain(\n 376|                &stop_sup,\n 377|                &restart,"}],"details":{"scopePath":"crates/spt-daemon, crates/spt","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":67,"fileCount":14,"files":["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-daemon/tests/false_promote.rs","crates/spt/src/cli.rs","crates/spt/tests/brain_survive.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"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":14},{"path":"crates/spt-daemon/src/brain.rs","count":3},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":5},{"path":"crates/spt-daemon/src/daemon.rs","count":1},{"path":"crates/spt-daemon/src/lib.rs","count":1},{"path":"crates/spt-daemon/src/msg.rs","count":1},{"path":"crates/spt-daemon/src/seedmap.rs","count":1},{"path":"crates/spt-daemon/tests/false_promote.rs","count":2},{"path":"crates/spt/src/cli.rs","count":7},{"path":"crates/spt/tests/brain_survive.rs","count":2},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":6},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":2},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":2}],"truncated":false,"displayContent":"# crates/\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 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 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 809│        // Wire the supervisor signal; the verb now raises it and reports honored.\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 828│    #[test]\n*829│    fn refresh_brain_cycles_without_staging_or_swap_preconditions() {\n 830│        let name = unique_name();\n 831│        let broker = served_broker(&name);\n 832│\n    │...\n 834│        assert!(\n*835│            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n 836│            \"no supervisor wired must report honored=false\"\n 837│        );\n 838│\n    │...\n 840│        // apply path rides — with nothing staged anywhere.\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 846│        );\n 847│        assert!(\n#### brain.rs\n   37│use crate::msg::{\n  *38│    decode_bytes, encode_bytes, AdapterApplyReq, AppliedEvent, BrainRestarted, BrokerImageReply, StallEvictsReply, DisplacedEvent,\n   39│    EndpointInjected, EndpointInputReq, ErrorEvent, ExitEvent,\n   40│    InputReq,\n   41│    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\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 1467│                }\n#### brainproc.rs\n  114│#[derive(Default)]\n *115│pub struct BrainRestart {\n  116│    requested: AtomicBool,\n  117│}\n  118│\n *119│impl BrainRestart {\n  120│    /// A fresh signal with no pending request.\n  121│    pub fn new() -> Self {\n  122│        Self::default()\n     │...\n  750│    stop: &AtomicBool,\n *751│    restart: &BrainRestart,\n  752│    base: Duration,\n  753│    env: &dyn TrialEnv,\n  754│    window: Duration,\n     │...\n 1091│/// seed-control loop. The brain child is supervised for the broker's lifetime.\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     │...\n 1098│    broker: Option<Arc<crate::broker::Broker>>,\n*1099│) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n*1100│    let restart = Arc::new(BrainRestart::new());\n 1101│    let restart_sup = Arc::clone(&restart);\n 1102│    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n 1103│    // start, before any `apply` can rename it under us. Linux current_exe()\n     │...\n 1183│        // Tiny base backoff so the test is quick; real production uses seconds.\n*1184│        let restart = BrainRestart::new();\n 1185│        supervise_brain(\n 1186│            &stop,\n 1187│            &restart,\n     │...\n 1204│\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     │...\n 1214│        let stop = Arc::new(AtomicBool::new(false));\n*1215│        let restart = Arc::new(BrainRestart::new());\n 1216│        let stop_c = Arc::clone(&stop);\n 1217│        let restart_c = Arc::clone(&restart);\n 1218│        let seen = Arc::new(std::sync::Mutex::new(Vec::<(u64, StartReason)>::new()));\n     │...\n 1258│        let seen_c = Arc::clone(&seen);\n*1259│        let restart = BrainRestart::new();\n 1260│        supervise_brain(\n 1261│            &stop,\n 1262│            &restart,\n     │...\n 1586│        let stop = Arc::new(AtomicBool::new(false));\n*1587│        let restart = BrainRestart::new();\n 1588│        let spawns = Arc::new(Mutex::new(Vec::<(u64, StartReason, Option<String>)>::new()));\n 1589│        let env_c = Arc::clone(&env);\n 1590│        let spawns_c = Arc::clone(&spawns);\n     │...\n 1640│        let stop = Arc::new(AtomicBool::new(false));\n*1641│        let restart = BrainRestart::new();\n 1642│        let env_c = Arc::clone(&env);\n 1643│        let stop_c = Arc::clone(&stop);\n 1644│        supervise_brain(\n     │...\n 1682│        let stop = Arc::new(AtomicBool::new(false));\n*1683│        let restart = BrainRestart::new();\n 1684│        let env_c = Arc::clone(&env);\n 1685│        let stop_c = Arc::clone(&stop);\n 1686│        let spawns = Arc::new(AtomicU32::new(0));\n     │...\n 1769│        let stop = Arc::new(AtomicBool::new(false));\n*1770│        let restart = BrainRestart::new();\n 1771│        let env_c = Arc::clone(&env);\n 1772│        let stop_c = Arc::clone(&stop);\n 1773│        let spawns = Arc::new(AtomicU32::new(0));\n     │...\n 1812│        let stop = Arc::new(AtomicBool::new(false));\n*1813│        let restart = BrainRestart::new();\n 1814│        let env_c = Arc::clone(&env);\n 1815│        let stop_c = Arc::clone(&stop);\n 1816│        let spawns = Arc::new(AtomicU32::new(0));\n     │...\n 1854│        let stop = Arc::new(AtomicBool::new(false));\n*1855│        let restart = BrainRestart::new();\n 1856│        let spawns = Arc::new(Mutex::new(Vec::<(u64, StartReason, Option<String>)>::new()));\n 1857│        let spawns_c = Arc::clone(&spawns);\n 1858│        let stop_c = Arc::clone(&stop);\n     │...\n 1909│        let stop = Arc::new(AtomicBool::new(false));\n*1910│        let restart = BrainRestart::new();\n 1911│        let candidate_pid = Arc::new(AtomicU32::new(0));\n 1912│        let cand_c = Arc::clone(&candidate_pid);\n 1913│        let stop_c = Arc::clone(&stop);\n     │...\n 1955│        let stop = Arc::new(AtomicBool::new(false));\n*1956│        let restart = BrainRestart::new();\n 1957│        let env_c = Arc::clone(&env);\n 1958│        let stop_c = Arc::clone(&stop);\n 1959│        supervise_brain(\n     │...\n 2003│        let stop = Arc::new(AtomicBool::new(false));\n*2004│        let restart = BrainRestart::new();\n 2005│        let stop_c = Arc::clone(&stop);\n 2006│        supervise_brain(\n 2007│            &stop,\n     │...\n 2086│        let stop = Arc::new(AtomicBool::new(false));\n*2087│        let restart = BrainRestart::new();\n 2088│        let spawns = Arc::new(Mutex::new(Vec::<Option<String>>::new()));\n 2089│        let spawns_c = Arc::clone(&spawns);\n 2090│        let stop_c = Arc::clone(&stop);\n#### broker.rs\n   55│    evicted_envelope, net_dialed_envelope, output_envelope,\n  *56│    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n   57│    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n   58│    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n   59│    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n     │...\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 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     │...\n 3894│            KIND_BRAIN_RESTARTED,\n*3895│            serde_json::to_value(BrainRestarted { honored })\n*3896│                .expect(\"BrainRestarted serializes\"),\n 3897│        );\n 3898│        send_frame(send, &frame);\n 3899│    }\n#### daemon.rs\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#### 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 876│#[derive(Debug, Clone, Serialize, Deserialize)]\n*877│pub struct BrainRestarted {\n 878│    #[serde(default)]\n 879│    pub honored: bool,\n 880│}\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#### false_promote.rs\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 298│    let sup = thread::spawn(move || {\n*299│        let restart = BrainRestart::new();\n 300│        supervise_brain(\n 301│            &stop_sup,\n 302│            &restart,\n\n## spt/\n\n### src/\n#### cli.rs\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 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\n### tests/\n#### brain_survive.rs\n  68│use spt_daemon::brain::Brain;\n *69│use spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};\n  70│use spt_daemon::codec::write_frame;\n  71│use spt_daemon::endpoint::broker_socket_name;\n  72│use spt_daemon::frame::{Envelope, Role};\n    │...\n 257│    let stop = Arc::new(AtomicBool::new(false));\n*258│    let restart = Arc::new(BrainRestart::new());\n 259│    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260│    let home_path = home.path().to_path_buf();\n 261│\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    │...\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#### resume_no_control_steal_e2e.rs\n  97│\n *98│use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n  99│use spt_daemon::codec::{read_frame, write_frame};\n 100│use spt_daemon::endpoint::broker_socket_name;\n 101│use spt_daemon::frame::{Envelope, Role};\n    │...\n 399│        thread::spawn(move || {\n*400│            let restart = BrainRestart::new();\n 401│            supervise_brain(\n 402│                &stop_sup,\n 403│                &restart,\n#### trial_drain_drive_e2e.rs\n 108│use spt_daemon::brain::Brain;\n*109│use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n 110│use spt_daemon::codec::{read_frame, write_frame};\n 111│use spt_daemon::endpoint::broker_socket_name;\n 112│use spt_daemon::frame::{Envelope, Role};\n    │...\n 373│        thread::spawn(move || {\n*374│            let restart = BrainRestart::new();\n 375│            supervise_brain(\n 376│                &stop_sup,\n 377│                &restart,"},"isError":false,"timestamp":1784195791787}}
{"type":"message","id":"c3e5570d","parentId":"40e574c4","timestamp":"2026-07-16T09:56:42.104Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting process-level supervisors and controllers**"},{"type":"toolCall","id":"call_pmpGrEnRRdYDonPji6Erb91h|fc_0b42584a8ab0545d016a58aada06d08199bc657e7141cbc3f4","name":"read","arguments":{"i":"Reading resume control E2E","path":"crates/spt/tests/resume_no_control_steal_e2e.rs","selector":"1-520"}},{"type":"toolCall","id":"call_p2jWQ7w4Orx8hfY3nfkrZxia|fc_0b42584a8ab0545d016a58aada06e08199b37d71c1d9233995","name":"read","arguments":{"i":"Reading trial drain E2E","path":"crates/spt/tests/trial_drain_drive_e2e.rs","selector":"1-520"}},{"type":"toolCall","id":"call_NRaLalZtsbvuBXMgnoFufk3U|fc_0b42584a8ab0545d016a58aada06e88199b82eecac2fe4efc4","name":"glob","arguments":{"i":"Locating mock session fixture","path":"crates/**/*mock*session*.rs;crates/**/mock-adapter/**;crates/**/mock_session.rs","hidden":true,"gitignore":true,"limit":100}},{"type":"toolCall","id":"call_zdgcOI4DrAgHherfQuXpdS41|fc_0b42584a8ab0545d016a58aada06f08199bc9571368b30b14f","name":"grep","arguments":{"i":"Finding multiplexer test helpers","pattern":"spawn_rc|rc_sees|Command::new\\(.*spt|args\\(\\[\"rc\"|stdin\\.take|stdout\\.take|write_all","path":"crates/spt/tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_DAAxfwCjfgSmbN5rr2PMVXSX|fc_0b42584a8ab0545d016a58aada07108199b282d0bd7d84b545","name":"grep","arguments":{"i":"Finding multi-session refresh tests","pattern":"const N|for .*sessions|RESTART|restart.request|daemon.*refresh|session.*survive|controller","path":"crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/trial_drain_drive_e2e.rs;crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":23773,"output":423,"cacheRead":142848,"cacheWrite":0,"totalTokens":167044,"reasoningTokens":59,"cost":{"input":0.11886500000000001,"output":0.01269,"cacheRead":0.071424,"cacheWrite":0,"total":0.20297900000000002}},"stopReason":"toolUse","timestamp":1784195791830,"responseId":"resp_0b42584a8ab0545d016a58aad0401c8199a9f188376d4e1a5d","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aad204348199b4c550f746772d50","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKra_6WBqNg9V4zMoOQ6_inA0Fxz-bAEjoTD4g_tUPCLez8JrhBcoT6SdYY1I9NzhmlhThpRNSLkWfb8SWlqRlvRsgtemSSBES8suIBS7-8LVau_Pxjj4XiPUsGxJaSFM-PbYUQ7U8GTo_xvebNpRY7EubkfUaOVtN0Ib4H5EPGhXdlqe2LtTXcWcoSKXg0G__PGjed3B8hI1D98xurRsvVvnQp_vhdodHwuOblnyQN9NeyCxF8C-DzsIpBnsJEflxZUmBfuT9kMypFkc_IhC9Vz0SGcMxEaG_eZjKToWYes2vAbe9DmCJTphKLPHZLSJtC27bJpNAEh6WGWWsZw4Ed5Ve30OPHmmuS1GGNMuPsxabwv0BsIIwd3QrJoDjK7pPosS1F5fwi5LlonnC4eWDgjK4w8QFYVnQiB_mTxbKNW0FXte0sr1XY5FV-j5Rnlg1sTzzqTlhL1jHby-eyXyTzZEupPyENYp4FOq_ndgMPLnBAVEj3J26i7--RPjgBhBHzTHNU7xfckB4Ked7pxp8DyPctX8xw1mc384wUe04d93mCYy0X8htyjKsl1FlxJT0Kqlh9LwxRVssl2t_N35QaVoWbwypNLmMbfsRewFWZhFaOhlfc9G3Ia3_otmTLkg__3BfMcaahMcsttZr7_KkkiI6-KBQOe5Pm-sv7D9YKjsO-r9qywOq5ttIZKTvj7ksb4dMIS0949THQpKw2iDtiWzV_5AxthOmMh8bRCekhBnr5icfxrR01vXqGqpBllyA9oQJeUx8-iTy4DE6jCbe07xRCVRWUHDotJ092XrU-dL7yEjSuNapdLY1BNxbx6TZ-9D2Ne9jucOQ8qIm_lviO0mkC7Und-VE7CijXY3tJ8crtQKcXR_UhaLjLuJQWurKGwQETIvvwMLovoLKidK6G8uOCL4yWD5Baiz3XPzYfdhqxUM4lDPRYdHB6nkmLk-dHLAFfv5uXGdmDx9y-n9BylT-XJ8gmxDWB2CBmDXlxHVKRawJi7Y7yWvMMtBCZE5nOdHFxWS6UgbAdnQxAb6Sevg60G_uJVI9ye3TdR_QWcxh4Fc925wwr-nRRWIXTYoMoYlOXaO_Nx4GJ5c9xbENBHovNBK65KulT5zrXNN3dSNYxzxXTnFH8Sv-DPh0OE2ERlU2vzLQZpSP30h8iF37wRUycYbEcLmPn2UucpXJO8OFAlhjYhg7gs6e68yCKQoWULxVnwmYTS3gLswcHbtw8nU-tgYDtNSYbnSnaGXN0pltQ=","summary":[{"type":"summary_text","text":"**Inspecting process-level supervisors and controllers**"}]},{"id":"fc_0b42584a8ab0545d016a58aada06d08199bc657e7141cbc3f4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading resume control E2E\",\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"selector\":\"1-520\"}","call_id":"call_pmpGrEnRRdYDonPji6Erb91h","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aada06e08199b37d71c1d9233995","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading trial drain E2E\",\"path\":\"crates/spt/tests/trial_drain_drive_e2e.rs\",\"selector\":\"1-520\"}","call_id":"call_p2jWQ7w4Orx8hfY3nfkrZxia","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aada06e88199b82eecac2fe4efc4","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating mock session fixture\",\"path\":\"crates/**/*mock*session*.rs;crates/**/mock-adapter/**;crates/**/mock_session.rs\",\"hidden\":true,\"gitignore\":true,\"limit\":100}","call_id":"call_NRaLalZtsbvuBXMgnoFufk3U","name":"glob"},{"id":"fc_0b42584a8ab0545d016a58aada06f08199bc9571368b30b14f","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding multiplexer test helpers\",\"pattern\":\"spawn_rc|rc_sees|Command::new\\\\(.*spt|args\\\\(\\\\[\\\"rc\\\"|stdin\\\\.take|stdout\\\\.take|write_all\",\"path\":\"crates/spt/tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_zdgcOI4DrAgHherfQuXpdS41","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aada07108199b282d0bd7d84b545","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding multi-session refresh tests\",\"pattern\":\"const N|for .*sessions|RESTART|restart.request|daemon.*refresh|session.*survive|controller\",\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/trial_drain_drive_e2e.rs;crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_DAAxfwCjfgSmbN5rr2PMVXSX","name":"grep"}]},"duration":10273.57040000055,"ttft":2179.5793000003323,"contextSnapshot":{"promptTokens":166621,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pmpGrEnRRdYDonPji6Erb91h|fc_0b42584a8ab0545d016a58aada06d08199bc657e7141cbc3f4","toolName":"read","startedAt":"2026-07-16T09:56:42.105Z","args":{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs"},"intent":"Reading resume control E2E"},"id":"29614562","parentId":"c3e5570d","timestamp":"2026-07-16T09:56:42.105Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_p2jWQ7w4Orx8hfY3nfkrZxia|fc_0b42584a8ab0545d016a58aada06e08199b37d71c1d9233995","toolName":"read","startedAt":"2026-07-16T09:56:42.105Z","args":{"path":"crates/spt/tests/trial_drain_drive_e2e.rs"},"intent":"Reading trial drain E2E"},"id":"962ed94f","parentId":"29614562","timestamp":"2026-07-16T09:56:42.105Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NRaLalZtsbvuBXMgnoFufk3U|fc_0b42584a8ab0545d016a58aada06e88199b82eecac2fe4efc4","toolName":"glob","startedAt":"2026-07-16T09:56:42.105Z","args":{"path":"crates/**/*mock*session*.rs;crates/**/mock-adapter/**;crates/**/mock_session.rs"},"intent":"Locating mock session fixture"},"id":"814ccb24","parentId":"962ed94f","timestamp":"2026-07-16T09:56:42.105Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zdgcOI4DrAgHherfQuXpdS41|fc_0b42584a8ab0545d016a58aada06f08199bc9571368b30b14f","toolName":"grep","startedAt":"2026-07-16T09:56:42.105Z","args":{"path":"crates/spt/tests"},"intent":"Finding multiplexer test helpers"},"id":"989e893f","parentId":"814ccb24","timestamp":"2026-07-16T09:56:42.105Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DAAxfwCjfgSmbN5rr2PMVXSX|fc_0b42584a8ab0545d016a58aada07108199b282d0bd7d84b545","toolName":"grep","startedAt":"2026-07-16T09:56:42.105Z","args":{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/trial_drain_drive_e2e.rs;crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs"},"intent":"Finding multi-session refresh tests"},"id":"60c28967","parentId":"989e893f","timestamp":"2026-07-16T09:56:42.105Z"}
{"type":"message","id":"e2f72a66","parentId":"60c28967","timestamp":"2026-07-16T09:56:42.108Z","message":{"role":"toolResult","toolCallId":"call_pmpGrEnRRdYDonPji6Erb91h|fc_0b42584a8ab0545d016a58aada06d08199bc657e7141cbc3f4","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//! brain derives `broker_socket_name()` from it — the brain_survive.rs / trial_drain_drive\n//! discipline). A SECOND `#[test]` MUST bring env-lock serialization or it races this one.\n//! `cargo nextest run --test resume_no_control_steal_e2e` or\n//! `cargo test --test resume_no_control_steal_e2e -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\n//!\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows installer-detection\n//! demands UAC elevation for such an exe → `os error 740` at `--list` (KH win-update-exe-\n//! uac740). This file is deliberately `resume_no_control_steal_e2e`.\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::endpoint::broker_socket_name;\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::relcache::AppliedRecord;\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// How many concurrent broker PTY sessions (each with its own local controller) the\n/// respawn re-attaches. ≥2 to be a genuine MULTI-session rig (the A'-rig had 1); 3 mirrors\n/// the field's \"several PTYs, some stolen\" without over-taxing CI.\nconst N_SESSIONS: usize = 3;\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\n/// A child that ticks a line every ~150ms FOREVER (until reaped). Each tick is a\n/// `KIND_OUTPUT` frame to the session's controller — the signal whose CONTINUED arrival at\n/// `A_i` proves `A_i` kept control across the respawn (GREEN) and whose CESSATION proves the\n/// resume STOLE control (RED). Unbuffered small writes so a tick reaches the PTY promptly.\nfn ticker_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\n        \"sh\".to_string(),\n        vec![\n            \"-c\".to_string(),\n            \"while true; do printf 'TICK\\\\n'; sleep 0.15; done\".to_string(),\n        ],\n    );\n    #[cfg(windows)]\n    let (program, args) = (\n        \"powershell\".to_string(),\n        vec![\n            \"-NoProfile\".to_string(),\n            \"-NonInteractive\".to_string(),\n            \"-Command\".to_string(),\n            \"while ($true) { [Console]::Out.WriteLine('TICK'); Start-Sleep -Milliseconds 150 }\"\n                .to_string(),\n        ],\n    );\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/// A harmless short-lived child for the rollback spawn slot (never reached on the GREEN\n/// promote path; on a RED rollback `record_rolled_back` has already raised `stop`, so the\n/// supervisor exits before it runs — this only satisfies the closure's `io::Result<Child>`).\nfn placeholder_child() -> std::io::Result<Child> {\n    #[cfg(windows)]\n    {\n        Command::new(\"cmd\")\n            .no_window()\n            .args([\"/C\", \"ping\", \"-n\", \"3\", \"127.0.0.1\"])\n            .stdout(std::process::Stdio::null())\n            .spawn()\n    }\n    #[cfg(unix)]\n    {\n        Command::new(\"sleep\").arg(\"2\").spawn()\n    }\n}\n\n/// A hermetic net config (mirrors trial_drain_drive_e2e / brain_survive): relays disabled,\n/// no discovery, loopback-only — no real interface, no firewall prompt. Net enabled so the\n/// subprocess brain boots the proven production way.\nfn hermetic(identity: Identity) -> NetConfig {\n    NetConfig {\n        identity,\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\n/// Bind a net-enabled broker (serving on a background thread) at `name`.\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n        .expect(\"bind broker with net\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\n/// Connect a raw brain-role conn to the broker (retrying while it finishes binding). The\n/// concrete stream is `Send + 'static` so the per-session reader thread can own it.\nfn connect(name: &str) -> impl std::io::Read + std::io::Write + Send + 'static {\n    for _ in 0..300 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Spawn one ticker session over a fresh controller conn and start a reader thread that\n/// tallies its `KIND_OUTPUT` frames. Returns `(session_id, tick_counter)`. `dispatch_spawn`\n/// pre-attaches this conn as the session's LOCAL (by:None) controller, so the tally IS the\n/// controller's live output feed — it FREEZES the instant a resume-steal displaces it.\nfn spawn_ticker_controller(name: &str, endpoint: &str) -> (u64, Arc<AtomicU64>) {\n    let mut c = connect(name);\n    send(\n        &mut c,\n        KIND_SPAWN,\n        serde_json::to_value(ticker_spawn_req(endpoint)).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut c).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    let ticks = Arc::new(AtomicU64::new(0));\n    let ticks_reader = Arc::clone(&ticks);\n    // The reader owns the conn for the rest of the test; process-per-test exit reaps it.\n    // A dropped controller sink (RED steal) stops the broker delivering to this conn — the\n    // read either EOFs (thread exits) or simply starves; either way the tally freezes.\n    thread::spawn(move || {\n        let mut c = c;\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    ticks_reader.fetch_add(1, Ordering::Relaxed);\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n    });\n    (sid, ticks)\n}\n\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + RAN resume_sessions +\n/// stamped ready — resume completes BEFORE ready, so a present ready means the steal, if\n/// any, has already happened). Returns that pid, or `None` if it never appears in `budget`.\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some(pid) = std::fs::read_to_string(path)\n            .ok()\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\n        {\n            return Some(pid as u32);\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads REAL\n/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\nstruct ResumeTrialEnv {\n    broker: Arc<Broker>,\n    ready_path: PathBuf,\n    promotions: Mutex<Vec<u64>>,\n    rollbacks: Mutex<Vec<u64>>,\n    stop: Arc<AtomicBool>,\n}\n\nimpl TrialEnv for ResumeTrialEnv {\n    fn applied_state(&self) -> Option<AppliedRecord> {\n        Some(AppliedRecord::AppliedPending {\n            version: 9,\n            rollback_binary: \"/good/spt.old-9\".to_string(),\n            candidate_started_ms: 0,\n            prior_version: None,\n        })\n    }\n    fn ready_generation(&self) -> Option<u64> {\n        ready_generation_at(&self.ready_path)\n    }\n    fn clear_ready(&self) {\n        let _ = std::fs::remove_file(&self.ready_path);\n    }\n    fn record_promoted(&self, version: u64) {\n        self.promotions.lock().unwrap().push(version);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\n        self.rollbacks.lock().unwrap().push(quarantine);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn ready_exe_hash(&self) -> Option<String> {\n        None\n    }\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n        None\n    }\n    // No wedge in this rig → drained is true → the latched-ready candidate promotes.\n    fn old_gen_drained(&self) -> bool {\n        !self.broker.any_local_controller_wedged()\n    }\n}\n\n/// Raise `stop` (so the supervisor kills any live brain child and exits) and panic with\n/// `msg` — keeps a failing assertion from orphaning the subprocess brain + the children.\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n    stop.store(true, Ordering::SeqCst);\n    panic!(\"{msg}\");\n}\n\n// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n#[test]\nfn brain_respawn_keeps_every_session_controller_and_still_promotes() {\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    std::env::set_var(\"SPT_HOME\", home.path());\n    // Parity with trial_drain_drive: shrink the broker-side brain-write deadline (grants\n    // wall-clock only, never relaxes an assertion) and silence the subprocess livehost\n    // reconcile loop so the boot is a clean, isolated real brain.\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"800\");\n    std::env::set_var(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\");\n\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n\n    // ── The in-process broker (stable kernel), bound at the SPT_HOME-derived name the real\n    //    subprocess brain cold-starts against. ──\n    let broker = net_broker(&broker_socket_name(), &home.path().join(\"broker\"));\n\n    // ── Establish N broker PTY sessions BEFORE the brain boots, so the candidate's\n    //    boot-once resume_sessions() COVERS them (the field condition: live PTYs present at\n    //    the respawn). Each conn A_i is the session's local (by:None) controller. ──\n    let mut sessions: Vec<(u64, Arc<AtomicU64>)> = Vec::with_capacity(N_SESSIONS);\n    for i in 0..N_SESSIONS {\n        sessions.push(spawn_ticker_controller(\n            &broker_socket_name(),\n            &format!(\"resume-steal-ep-{i}\"),\n        ));\n    }\n\n    let stop = Arc::new(AtomicBool::new(false));\n\n    // ── Baseline: every A_i must be RECEIVING ticks (its child is producing and A_i holds\n    //    control) before the respawn, or the discriminator is meaningless. ──\n    let baseline_deadline = Instant::now() + Duration::from_secs(20);\n    for (sid, ticks) in &sessions {\n        while ticks.load(Ordering::Relaxed) == 0 {\n            if Instant::now() >= baseline_deadline {\n                teardown_panic(\n                    &stop,\n                    &format!(\"PRECONDITION: session {sid} controller never received a tick — \\\n                              the child never produced output; the rig cannot show a steal\"),\n                );\n            }\n            thread::sleep(Duration::from_millis(25));\n        }\n    }\n\n    let ready_path = home.path().join(\"brain.ready\");\n    let env = Arc::new(ResumeTrialEnv {\n        broker: Arc::clone(&broker),\n        ready_path: ready_path.clone(),\n        promotions: Mutex::new(Vec::new()),\n        rollbacks: Mutex::new(Vec::new()),\n        stop: Arc::clone(&stop),\n    });\n\n    // ── Drive the REAL promotion path (supervise_brain → run_trial), spawning the REAL\n    //    `spt daemon brain` child running production run_brain → resume_sessions (which\n    //    re-attaches the N sessions: Control=steal in RED, Viewer=keep in GREEN). ──\n    let home_path = home.path().to_path_buf();\n    let stop_spawn = Arc::clone(&stop);\n    let sup = {\n        let stop_sup = Arc::clone(&stop);\n        let env_sup = Arc::clone(&env);\n        thread::spawn(move || {\n            let restart = BrainRestart::new();\n            supervise_brain(\n                &stop_sup,\n                &restart,\n                Duration::from_millis(50),\n                env_sup.as_ref(),\n                Duration::from_secs(60),\n                move |gen, reason: StartReason, binary| {\n                    if binary.is_some() {\n                        // A rollback spawn means the trial FAILED to promote — end the run\n                        // rather than launch the bogus rollback binary (record_rolled_back\n                        // already raised stop).\n                        stop_spawn.store(true, Ordering::Relaxed);\n                        return placeholder_child();\n                    }\n                    Command::new(&spt_bin)\n                        .no_window()\n                        .args([\n                            \"daemon\",\n                            \"brain\",\n                            \"--generation\",\n                            &gen.to_string(),\n                            \"--start-reason\",\n                            reason.as_arg(),\n                        ])\n                        .env(\"SPT_HOME\", &home_path)\n                        .env(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\")\n                        .spawn()\n                },\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\n    // Assertion 3 (no-regression guard): the trial still PROMOTES — a respawn under N live\n    // controllers must not break REQ-UPDATE-TRIAL-DRAIN-DRIVE. With no controller wedged,\n    // old_gen_drained() is true and the ready candidate promotes.\n    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    assert!(\n        promoted,\n        \"the trial must still PROMOTE the candidate under N live sessions (no regression of \\\n         REQ-UPDATE-TRIAL-DRAIN-DRIVE): with the Viewer re-attach no controller is wedged, so \\\n         old_gen_drained() is true and the ready candidate promotes. rollbacks={rollbacks:?}\"\n    );\n    assert_eq!(\n        promotions.as_slice(),\n        &[9],\n        \"promoted the candidate version exactly once\"\n    );\n}\n"}],"details":{"displayContent":{"text":"//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\r\n//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\r\n//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\r\n//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\r\n//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\r\n//! resume-steal bug needed and the single-session rig never exercised.\r\n//!\r\n//! ## The field incident this locks down (root, log-anchored — docs/UPDATE-WEDGE-2-RCA.md)\r\n//!\r\n//! `spt update fetch --apply` (v0.30.2 → v0.30.3) PROMOTED CLEANLY (the counter-54 fix\r\n//! worked) but under 7 live spt-hosted broker PTYs it FROZE all 7 + failed every `spt rc`\r\n//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\r\n//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\r\n//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\r\n//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\r\n//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\r\n//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\r\n//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\r\n//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\r\n//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\r\n//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\r\n//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\r\n//! journaled `rc` retake deadlined (Failure B).\r\n//!\r\n//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\r\n//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\r\n//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\r\n//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\r\n//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\r\n//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\r\n//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\r\n//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\r\n//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\r\n//! never evict (the field's 2) — but ALL are silently stolen+frozen. The 5-vs-2 split is\r\n//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\r\n//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\r\n//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\r\n//!\r\n//! ## The FIX and what this rig discriminates\r\n//!\r\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\r\n//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\r\n//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\r\n//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\r\n//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\r\n//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\r\n//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\r\n//!\r\n//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\r\n//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\r\n//! this rig asserts is\r\n//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\r\n//! ROUTING:\r\n//!\r\n//! - Each session is spawned by a controller conn (`A_i`) the test HOLDS; `dispatch_spawn`\r\n//!   pre-attaches `A_i` as the session's LOCAL (by:None) controller. A reader thread on\r\n//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms).\r\n//! - The real `spt daemon brain` candidate boots and `resume_sessions()` re-attaches all N.\r\n//! - **RED (Control re-attach):** `resolve_subscribe` `same_identity(None==None)` →\r\n//!   `become_controller` DROPS `A_i`'s sink → `A_i` is displaced → its tick counter FREEZES.\r\n//! - **GREEN (Viewer re-attach):** the brain joins as a viewer → `A_i` stays controller →\r\n//!   its tick counter KEEPS climbing across the swap.\r\n//!\r\n//! So the load-bearing assertion is DETERMINISTIC (output-routing displacement), not the\r\n//! flaky 15s wedge-maturity timing: RED starves every `A_i` the instant resume runs; GREEN\r\n//! never does. We also assert the trial still PROMOTES (no regression of\r\n//! REQ-UPDATE-TRIAL-DRAIN-DRIVE) — with no controller wedged, `old_gen_drained()` is true\r\n//! and the latched-ready candidate promotes.\r\n//!\r\n//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\r\n//! to doyle 2026-07-09). Failure B is the daemon-brain-conn SATURATION symptom; in an\r\n//! in-proc broker a local `rc` (by:None) `same_identity`-TAKES at the broker even in the RED\r\n//! state, so it does not discriminate here, and forcing real subprocess-brain-conn\r\n//! saturation is brittle (the same nondeterminism the counter-54 rig engineered around). We\r\n//! assert the ROOT (the steal) that CAUSES B; a full-daemon B repro is left to PR CI /\r\n//! field verification per doyle's ruling (heavy E2Es off the local gate).\r\n//!\r\n//! ## RUNNER CONTRACT — single `#[test]`, process-per-test (nextest) or -j1\r\n//!\r\n//! The SOLE `#[test]` here by design: it owns the process-global `SPT_HOME` (the subprocess\r\n//! brain derives `broker_socket_name()` from it — the brain_survive.rs / trial_drain_drive\r\n//! discipline). A SECOND `#[test]` MUST bring env-lock serialization or it races this one.\r\n//! `cargo nextest run --test resume_no_control_steal_e2e` or\r\n//! `cargo test --test resume_no_control_steal_e2e -- --test-threads=1`. Never a bare\r\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\r\n//!\r\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows installer-detection\r\n//! demands UAC elevation for such an exe → `os error 740` at `--list` (KH win-update-exe-\r\n//! uac740). This file is deliberately `resume_no_control_steal_e2e`.\r\n\r\nuse std::path::PathBuf;\r\nuse std::process::{Child, Command};\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::{Arc, Mutex};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::endpoint::broker_socket_name;\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED};\r\nuse spt_daemon::nethost::{NetConfig, NetHost};\r\nuse spt_daemon::relcache::AppliedRecord;\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\r\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\r\nuse spt_proto::identity::Identity;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\n/// How many concurrent broker PTY sessions (each with its own local controller) the\r\n/// respawn re-attaches. ≥2 to be a genuine MULTI-session rig (the A'-rig had 1); 3 mirrors\r\n/// the field's \"several PTYs, some stolen\" without over-taxing CI.\r\nconst N_SESSIONS: usize = 3;\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\n/// A child that ticks a line every ~150ms FOREVER (until reaped). Each tick is a\r\n/// `KIND_OUTPUT` frame to the session's controller — the signal whose CONTINUED arrival at\r\n/// `A_i` proves `A_i` kept control across the respawn (GREEN) and whose CESSATION proves the\r\n/// resume STOLE control (RED). Unbuffered small writes so a tick reaches the PTY promptly.\r\nfn ticker_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\r\n            \"-c\".to_string(),\r\n            \"while true; do printf 'TICK\\\\n'; sleep 0.15; done\".to_string(),\r\n        ],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"powershell\".to_string(),\r\n        vec![\r\n            \"-NoProfile\".to_string(),\r\n            \"-NonInteractive\".to_string(),\r\n            \"-Command\".to_string(),\r\n            \"while ($true) { [Console]::Out.WriteLine('TICK'); Start-Sleep -Milliseconds 150 }\"\r\n                .to_string(),\r\n        ],\r\n    );\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/// A harmless short-lived child for the rollback spawn slot (never reached on the GREEN\r\n/// promote path; on a RED rollback `record_rolled_back` has already raised `stop`, so the\r\n/// supervisor exits before it runs — this only satisfies the closure's `io::Result<Child>`).\r\nfn placeholder_child() -> std::io::Result<Child> {\r\n    #[cfg(windows)]\r\n    {\r\n        Command::new(\"cmd\")\r\n            .no_window()\r\n            .args([\"/C\", \"ping\", \"-n\", \"3\", \"127.0.0.1\"])\r\n            .stdout(std::process::Stdio::null())\r\n            .spawn()\r\n    }\r\n    #[cfg(unix)]\r\n    {\r\n        Command::new(\"sleep\").arg(\"2\").spawn()\r\n    }\r\n}\r\n\r\n/// A hermetic net config (mirrors trial_drain_drive_e2e / brain_survive): relays disabled,\r\n/// no discovery, loopback-only — no real interface, no firewall prompt. Net enabled so the\r\n/// subprocess brain boots the proven production way.\r\nfn hermetic(identity: Identity) -> NetConfig {\r\n    NetConfig {\r\n        identity,\r\n        relay: RelayPolicy::Disabled,\r\n        discovery: LocalDiscovery::Off,\r\n        scope: BindScope::Loopback,\r\n        membership: None,\r\n    }\r\n}\r\n\r\n/// Bind a net-enabled broker (serving on a background thread) at `name`.\r\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\r\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\r\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\r\n        .expect(\"bind broker with net\");\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 send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\r\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\r\n}\r\n\r\n/// Connect a raw brain-role conn to the broker (retrying while it finishes binding). The\r\n/// concrete stream is `Send + 'static` so the per-session reader thread can own it.\r\nfn connect(name: &str) -> impl std::io::Read + std::io::Write + Send + 'static {\r\n    for _ in 0..300 {\r\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\r\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\r\n            return c;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"could not connect to broker {name}\");\r\n}\r\n\r\n/// Spawn one ticker session over a fresh controller conn and start a reader thread that\r\n/// tallies its `KIND_OUTPUT` frames. Returns `(session_id, tick_counter)`. `dispatch_spawn`\r\n/// pre-attaches this conn as the session's LOCAL (by:None) controller, so the tally IS the\r\n/// controller's live output feed — it FREEZES the instant a resume-steal displaces it.\r\nfn spawn_ticker_controller(name: &str, endpoint: &str) -> (u64, Arc<AtomicU64>) {\r\n    let mut c = connect(name);\r\n    send(\r\n        &mut c,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(ticker_spawn_req(endpoint)).unwrap(),\r\n    );\r\n    let sid = loop {\r\n        let f = read_frame(&mut c).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\r\n        }\r\n    };\r\n    let ticks = Arc::new(AtomicU64::new(0));\r\n    let ticks_reader = Arc::clone(&ticks);\r\n    // The reader owns the conn for the rest of the test; process-per-test exit reaps it.\r\n    // A dropped controller sink (RED steal) stops the broker delivering to this conn — the\r\n    // read either EOFs (thread exits) or simply starves; either way the tally freezes.\r\n    thread::spawn(move || {\r\n        let mut c = c;\r\n        loop {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == KIND_OUTPUT => {\r\n                    ticks_reader.fetch_add(1, Ordering::Relaxed);\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => break,\r\n            }\r\n        }\r\n    });\r\n    (sid, ticks)\r\n}\r\n\r\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + RAN resume_sessions +\r\n/// stamped ready — resume completes BEFORE ready, so a present ready means the steal, if\r\n/// any, has already happened). Returns that pid, or `None` if it never appears in `budget`.\r\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some(pid) = std::fs::read_to_string(path)\r\n            .ok()\r\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\r\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\r\n        {\r\n            return Some(pid as u32);\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads REAL\r\n/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\r\n/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\r\n/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\r\n/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\r\nstruct ResumeTrialEnv {\r\n    broker: Arc<Broker>,\r\n    ready_path: PathBuf,\r\n    promotions: Mutex<Vec<u64>>,\r\n    rollbacks: Mutex<Vec<u64>>,\r\n    stop: Arc<AtomicBool>,\r\n}\r\n\r\nimpl TrialEnv for ResumeTrialEnv {\r\n    fn applied_state(&self) -> Option<AppliedRecord> {\r\n        Some(AppliedRecord::AppliedPending {\r\n            version: 9,\r\n            rollback_binary: \"/good/spt.old-9\".to_string(),\r\n            candidate_started_ms: 0,\r\n            prior_version: None,\r\n        })\r\n    }\r\n    fn ready_generation(&self) -> Option<u64> {\r\n        ready_generation_at(&self.ready_path)\r\n    }\r\n    fn clear_ready(&self) {\r\n        let _ = std::fs::remove_file(&self.ready_path);\r\n    }\r\n    fn record_promoted(&self, version: u64) {\r\n        self.promotions.lock().unwrap().push(version);\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\r\n        self.rollbacks.lock().unwrap().push(quarantine);\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n    fn ready_exe_hash(&self) -> Option<String> {\r\n        None\r\n    }\r\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\r\n        None\r\n    }\r\n    // No wedge in this rig → drained is true → the latched-ready candidate promotes.\r\n    fn old_gen_drained(&self) -> bool {\r\n        !self.broker.any_local_controller_wedged()\r\n    }\r\n}\r\n\r\n/// Raise `stop` (so the supervisor kills any live brain child and exits) and panic with\r\n/// `msg` — keeps a failing assertion from orphaning the subprocess brain + the children.\r\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\r\n    stop.store(true, Ordering::SeqCst);\r\n    panic!(\"{msg}\");\r\n}\r\n\r\n// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n#[test]\r\nfn brain_respawn_keeps_every_session_controller_and_still_promotes() {\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n    // Parity with trial_drain_drive: shrink the broker-side brain-write deadline (grants\r\n    // wall-clock only, never relaxes an assertion) and silence the subprocess livehost\r\n    // reconcile loop so the boot is a clean, isolated real brain.\r\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"800\");\r\n    std::env::set_var(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\");\r\n\r\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n\r\n    // ── The in-process broker (stable kernel), bound at the SPT_HOME-derived name the real\r\n    //    subprocess brain cold-starts against. ──\r\n    let broker = net_broker(&broker_socket_name(), &home.path().join(\"broker\"));\r\n\r\n    // ── Establish N broker PTY sessions BEFORE the brain boots, so the candidate's\r\n    //    boot-once resume_sessions() COVERS them (the field condition: live PTYs present at\r\n    //    the respawn). Each conn A_i is the session's local (by:None) controller. ──\r\n    let mut sessions: Vec<(u64, Arc<AtomicU64>)> = Vec::with_capacity(N_SESSIONS);\r\n    for i in 0..N_SESSIONS {\r\n        sessions.push(spawn_ticker_controller(\r\n            &broker_socket_name(),\r\n            &format!(\"resume-steal-ep-{i}\"),\r\n        ));\r\n    }\r\n\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n\r\n    // ── Baseline: every A_i must be RECEIVING ticks (its child is producing and A_i holds\r\n    //    control) before the respawn, or the discriminator is meaningless. ──\r\n    let baseline_deadline = Instant::now() + Duration::from_secs(20);\r\n    for (sid, ticks) in &sessions {\r\n        while ticks.load(Ordering::Relaxed) == 0 {\r\n            if Instant::now() >= baseline_deadline {\r\n                teardown_panic(\r\n                    &stop,\r\n                    &format!(\"PRECONDITION: session {sid} controller never received a tick — \\\r\n                              the child never produced output; the rig cannot show a steal\"),\r\n                );\r\n            }\r\n            thread::sleep(Duration::from_millis(25));\r\n        }\r\n    }\r\n\r\n    let ready_path = home.path().join(\"brain.ready\");\r\n    let env = Arc::new(ResumeTrialEnv {\r\n        broker: Arc::clone(&broker),\r\n        ready_path: ready_path.clone(),\r\n        promotions: Mutex::new(Vec::new()),\r\n        rollbacks: Mutex::new(Vec::new()),\r\n        stop: Arc::clone(&stop),\r\n    });\r\n\r\n    // ── Drive the REAL promotion path (supervise_brain → run_trial), spawning the REAL\r\n    //    `spt daemon brain` child running production run_brain → resume_sessions (which\r\n    //    re-attaches the N sessions: Control=steal in RED, Viewer=keep in GREEN). ──\r\n    let home_path = home.path().to_path_buf();\r\n    let stop_spawn = Arc::clone(&stop);\r\n    let sup = {\r\n        let stop_sup = Arc::clone(&stop);\r\n        let env_sup = Arc::clone(&env);\r\n        thread::spawn(move || {\r\n            let restart = BrainRestart::new();\r\n            supervise_brain(\r\n                &stop_sup,\r\n                &restart,\r\n                Duration::from_millis(50),\r\n                env_sup.as_ref(),\r\n                Duration::from_secs(60),\r\n                move |gen, reason: StartReason, binary| {\r\n                    if binary.is_some() {\r\n                        // A rollback spawn means the trial FAILED to promote — end the run\r\n                        // rather than launch the bogus rollback binary (record_rolled_back\r\n                        // already raised stop).\r\n                        stop_spawn.store(true, Ordering::Relaxed);\r\n                        return placeholder_child();\r\n                    }\r\n                    Command::new(&spt_bin)\r\n                        .no_window()\r\n                        .args([\r\n                            \"daemon\",\r\n                            \"brain\",\r\n                            \"--generation\",\r\n                            &gen.to_string(),\r\n                            \"--start-reason\",\r\n                            reason.as_arg(),\r\n                        ])\r\n                        .env(\"SPT_HOME\", &home_path)\r\n                        .env(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\")\r\n                        .spawn()\r\n                },\r\n            );\r\n        })\r\n    };\r\n\r\n    // ── The candidate boots, RUNS resume_sessions against the N present sessions, then\r\n    //    stamps ready. A present ready ⇒ resume (and the RED steal, if any) has completed. ──\r\n    let _cand_pid = wait_ready_pid(&ready_path, Duration::from_secs(45))\r\n        .unwrap_or_else(|| teardown_panic(&stop, \"the trial candidate never stamped brain.ready\"));\r\n\r\n    // ── DISCRIMINATOR (load-bearing, RED-first). Snapshot each A_i tick tally AT the\r\n    //    post-resume instant, then measure a window. GREEN (Viewer re-attach): A_i kept\r\n    //    control → its tally CLIMBS. RED (Control re-attach): resume become_controller'd the\r\n    //    slot → A_i's sink was dropped → its tally FREEZES. The child keeps ticking either\r\n    //    way (broker-hosted, independent of the brain), so a frozen tally is a displaced\r\n    //    controller, not a dead child. ──\r\n    let t0: Vec<u64> = sessions.iter().map(|(_, t)| t.load(Ordering::Relaxed)).collect();\r\n    // ~2s ≫ the 150ms tick, so GREEN accrues ~10+ ticks; comfortably clears CI jitter.\r\n    thread::sleep(Duration::from_secs(2));\r\n    let t1: Vec<u64> = sessions.iter().map(|(_, t)| t.load(Ordering::Relaxed)).collect();\r\n\r\n    // ── Wait out the trial verdict (assertion 3). With no wedged controller, GREEN drains\r\n    //    true and promotes within a heartbeat of ready; a RED that also wedged its stolen\r\n    //    controller may instead roll back — either way the discriminator above already fired.\r\n    let deadline = Instant::now() + Duration::from_secs(30);\r\n    let promoted = loop {\r\n        if !env.promotions.lock().unwrap().is_empty() {\r\n            break true;\r\n        }\r\n        if !env.rollbacks.lock().unwrap().is_empty() || Instant::now() >= deadline {\r\n            break false;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    };\r\n\r\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess brain +\r\n    //    the N ticker children). ──\r\n    stop.store(true, Ordering::Relaxed);\r\n    let child_pids: Vec<Option<u32>> = sessions.iter().map(|(sid, _)| broker.session_pid(*sid)).collect();\r\n    for pid in child_pids.into_iter().flatten() {\r\n        kill_pid(pid);\r\n    }\r\n    let _ = sup.join();\r\n\r\n    let promotions = env.promotions.lock().unwrap().clone();\r\n    let rollbacks = env.rollbacks.lock().unwrap().clone();\r\n    let gained: Vec<u64> = t0.iter().zip(&t1).map(|(a, b)| b.saturating_sub(*a)).collect();\r\n    eprintln!(\r\n        \"=== UPDATE-WEDGE-2 RESUME-STEAL GATE: n={N_SESSIONS} ticks_before={t0:?} \\\r\n         ticks_after={t1:?} gained_in_window={gained:?} promoted={promoted} \\\r\n         promotions={promotions:?} rollbacks={rollbacks:?} \\\r\n         (GREEN = every gained>0 + promoted; RED = gained==0 on the stolen sessions) ===\"\r\n    );\r\n\r\n    // Assertion 1 (load-bearing, RED-first): EVERY session kept its controller across the\r\n    // swap — each A_i went on receiving output after the respawn. RED (resume re-attaches\r\n    // AttachIntent::Control): resolve_subscribe same_identity(None==None) become_controller'd\r\n    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\r\n    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n    for (i, g) in gained.iter().enumerate() {\r\n        assert!(\r\n            *g > 0,\r\n            \"session {i} (sid {}) LOST its controller across the brain respawn: its \\\r\n             controller conn received {g} new output frames in the post-resume window (froze \\\r\n             at {}). The respawn's resume_sessions STOLE the controller slot (Control \\\r\n             re-attach → become_controller displaced the local controller) then never drained \\\r\n             it — Failure A. GREEN (resume re-attaches Viewer) leaves every controller intact. \\\r\n             ticks_before={t0:?} ticks_after={t1:?}\",\r\n            sessions[i].0,\r\n            t1[i],\r\n        );\r\n    }\r\n\r\n    // Assertion 3 (no-regression guard): the trial still PROMOTES — a respawn under N live\r\n    // controllers must not break REQ-UPDATE-TRIAL-DRAIN-DRIVE. With no controller wedged,\r\n    // old_gen_drained() is true and the ready candidate promotes.\r\n    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n    assert!(\r\n        promoted,\r\n        \"the trial must still PROMOTE the candidate under N live sessions (no regression of \\\r\n         REQ-UPDATE-TRIAL-DRAIN-DRIVE): with the Viewer re-attach no controller is wedged, so \\\r\n         old_gen_drained() is true and the ready candidate promotes. rollbacks={rollbacks:?}\"\r\n    );\r\n    assert_eq!(\r\n        promotions.as_slice(),\r\n        &[9],\r\n        \"promoted the candidate version exactly once\"\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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\resume_no_control_steal_e2e.rs"}}},"isError":false,"timestamp":1784195802108}}
{"type":"message","id":"7d84e0b4","parentId":"e2f72a66","timestamp":"2026-07-16T09:56:42.112Z","message":{"role":"toolResult","toolCallId":"call_p2jWQ7w4Orx8hfY3nfkrZxia|fc_0b42584a8ab0545d016a58aada06e08199b37d71c1d9233995","toolName":"read","content":[{"type":"text","text":"//! UPDATE-WEDGE counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) — the A' int rig\n//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\n//! production `run_brain` self-drives the broker's controller-liveness reap so a\n//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\n//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\n//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\n//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\n//! operator take; here the candidate is the real subprocess whose OWN heartbeat\n//! `Brain::sessions()` drives the reap — the exact production line the 2026-07-09\n//! field freeze proved missing.\n//!\n//! ## The field freeze this locks down (root, log-anchored)\n//!\n//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\n//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\n//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\n//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\n//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\n//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\n//! OWN trial with no coordinated broker restart.\n//!\n//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\n//!\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\n//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\n//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\n//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\n//! only makes this rig MORE faithful; it still establishes the wedge after boot for\n//! continuity. The historical `resume_sessions()` framing is retained below for context.\n//!\n//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\n//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\n//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\n//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\n//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\n//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\n//! flood session and black-holes as its by:None controller — a session the boot-once\n//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\n//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\n//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\n//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\n//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\n//! read BROKER-side, in THIS process's in-proc broker where the reap + the drained read\n//! both run.\n//!\n//! ## The DRAINED gate is held open deterministically (the ready latch)\n//!\n//! The real subprocess stamps `brain.ready` autonomously the instant it boots — before\n//! the test can establish the wedge. So the rig's [`TrialEnv::ready_generation`]\n//! WITHHOLDS the candidate's readiness from `run_trial` until the test ARMS it (after\n//! the old-gen wedge is up + matured), then reads the REAL `brain.ready` file. This is\n//! the process-level analog of false_promote's scripted-ready `Mutex`: it guarantees\n//! the DRAINED gate has a matured wedge to block on when readiness is first seen, so\n//! the promote is a genuine drive-cleared-the-wedge, never a trivial no-wedge promote.\n//!\n//! ## What this rig asserts — BOTH legs are \"the brain's OWN self-drive clears it\"\n//!\n//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\n//!   wedge and the candidate ready, the trial PROMOTES within the window — the\n//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\n//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\n//!   before commit): comment out the production drive `let _ = brain.sessions();`\n//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\n//!   test rolls back instead (`WindowElapsedAlive` → the field freeze) — proving the\n//!   drive is load-bearing. For the RED to bite, the run_brain heartbeat must be the\n//!   ONLY `KIND_SESSIONS` driver: the subprocess brain's livehost reconcile loop also\n//!   polls `brain.sessions()` every 5s and would drive the same reap, so the rig sets\n//!   `SPT_LIVEHOST_RECONCILE_DISABLE` to silence it — faithfully reproducing the field's\n//!   livehost-silent update-trial (PIN Q2: the trial-brain livehost's first reconcile\n//!   tick landed around the 30s kill), the exact condition the fix makes the reap\n//!   independent of.**\n//! - **Leg 2 (lighter, doyle FINALIZE refinement).** Assert the broker's stall-evict\n//!   tally INCREMENTED — a reap actually FIRED via the brain's own drive (the test never\n//!   polls `KIND_SESSIONS` itself, so the subprocess candidate is the ONLY driver). We\n//!   do NOT assert zero evicts: the stall-evict IS the reap mechanism, so a\n//!   count==0 assertion would be asserting the fix does not work. The recovery\n//!   (crash-reason) brain runs the IDENTICAL `run_brain` heartbeat, so this tally —\n//!   confirmed by the promote — is the same-code-path evidence that a rolled-back brain\n//!   also self-heals its control plane promptly on its own heartbeat, not an external poll.\n//!\n//! ## RUNNER CONTRACT — single `#[test]`, run process-per-test (nextest) or -j1\n//!\n//! This is the SOLE `#[test]` here by design: it owns the process-global `SPT_HOME`\n//! (the subprocess brain derives `broker_socket_name()` from it, so the in-proc broker\n//! and the child must share ONE home — the brain_survive.rs discipline). A SECOND\n//! `#[test]` added here MUST bring env-lock serialization or it races this one. Run with\n//! `cargo nextest run --test trial_drain_drive_e2e` (CI's path) or\n//! `cargo test --test trial_drain_drive_e2e -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\n//!\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\n//! installer-detection heuristics demand UAC elevation for a test exe whose name\n//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\n//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brain::Brain;\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::endpoint::broker_socket_name;\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::relcache::AppliedRecord;\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\n\nmod common;\nuse common::CommandNoWindowExt;\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\n/// A child that emits ONE large BURST then goes IDLE (never reads stdin). The burst\n/// (~1 MiB, single fast write) overflows the non-reading controller conn A's socket recv\n/// buffer so A's broker-side writer parks BLOCKED inside `write_frame` — setting\n/// `write_blocked_since` (the stall-evict/reap signal). Then the child is SILENT, so no\n/// further output reaches the drain: the ORTHOGONAL, output-driven `CONTROLLER_WRITE_DEADLINE`\n/// detach (5s, tripped only by a channel that stays FULL as new chunks arrive) never fires,\n/// and the wedge PERSISTS until the `BRAIN_WRITE_DEADLINE` reap (the fix's heartbeat drive)\n/// clears it. This is the FAITHFUL analog of the field's idle-brain black-hole (broker.rs\n/// BRAIN_WRITE_DEADLINE doc): a suspended brain conn with a stuck write on an idle session.\n/// A CONTINUOUS flood (false_promote.rs) instead trips the 5s output-driven detach and only\n/// works because that rig finishes < 5s — the slower subprocess rig would self-heal.\nfn burst_then_idle_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\n        \"sh\".to_string(),\n        vec![\n            \"-c\".to_string(),\n            \"yes FLOODFLOODFLOODFLOOD | head -c 1048576; sleep 600\".to_string(),\n        ],\n    );\n    #[cfg(windows)]\n    let (program, args) = (\n        \"powershell\".to_string(),\n        vec![\n            \"-NoProfile\".to_string(),\n            \"-NonInteractive\".to_string(),\n            \"-Command\".to_string(),\n            \"[Console]::Out.Write('F' * 1048576); Start-Sleep -Seconds 600\".to_string(),\n        ],\n    );\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/// A harmless short-lived child for the rollback spawn slot (never reached on the\n/// GREEN promote path; on a RED rollback `record_rolled_back` has already raised\n/// `stop`, so the supervisor exits before it runs — this only satisfies the\n/// closure's `io::Result<Child>` return).\nfn placeholder_child() -> std::io::Result<Child> {\n    #[cfg(windows)]\n    {\n        Command::new(\"cmd\")\n            .no_window()\n            .args([\"/C\", \"ping\", \"-n\", \"3\", \"127.0.0.1\"])\n            .stdout(std::process::Stdio::null())\n            .spawn()\n    }\n    #[cfg(unix)]\n    {\n        Command::new(\"sleep\").arg(\"2\").spawn()\n    }\n}\n\n/// A hermetic net config (mirrors brain_survive.rs / netbroker.rs): relays disabled,\n/// no discovery, loopback-only — no real interface touched, no firewall prompt. Net is\n/// enabled so the real subprocess brain boots the proven way (the field brain had net up\n/// too). The subprocess's aux `KIND_SESSIONS` driver — livehost's reconcile poll — is\n/// silenced via `SPT_LIVEHOST_RECONCILE_DISABLE` (see the module doc), so the ONLY reap\n/// driver left is the fix's core heartbeat, and the RED stays honest.\nfn hermetic(identity: Identity) -> NetConfig {\n    NetConfig {\n        identity,\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\n/// Bind a net-enabled broker (serving on a background thread) at `name`, effect\n/// journal under `dir`.\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n        .expect(\"bind broker with net\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\n/// Connect a raw brain conn to the broker (retrying while it finishes binding).\nfn connect(name: &str) -> impl std::io::Read + std::io::Write {\n    for _ in 0..300 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + resumed + stamped\n/// ready), returning that pid. `None` if it never appears in `budget`.\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some(pid) = std::fs::read_to_string(path)\n            .ok()\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\n        {\n            return Some(pid as u32);\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// The broker's cumulative stall-evict tally via `KIND_STALL_EVICTS` (a PURE tally read —\n/// it does NOT drive a reap, unlike a `KIND_SESSIONS` poll). `0` if unreachable/unreported.\nfn read_evicts(name: &str) -> u64 {\n    Brain::cold_start(name, 1)\n        .ok()\n        .and_then(|mut b| b.stall_evicts().ok().flatten())\n        .map(|(count, _last_ms)| count)\n        .unwrap_or(0)\n}\n\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads\n/// REAL broker truth (`any_local_controller_wedged`) off the live in-proc broker (the\n/// whole point), and whose readiness is LATCHED behind `armed` — withheld from\n/// `run_trial` until the test has established the old-gen wedge, then read from the\n/// REAL `brain.ready` file the subprocess wrote. `record_promoted` / `record_rolled_back`\n/// capture the verdict AND raise `stop` so `supervise_brain` returns.\nstruct DrainDriveEnv {\n    broker: Arc<Broker>,\n    ready_path: PathBuf,\n    armed: AtomicBool,\n    promotions: Mutex<Vec<u64>>,\n    rollbacks: Mutex<Vec<u64>>,\n    stop: Arc<AtomicBool>,\n}\n\nimpl TrialEnv for DrainDriveEnv {\n    fn applied_state(&self) -> Option<AppliedRecord> {\n        Some(AppliedRecord::AppliedPending {\n            version: 9,\n            rollback_binary: \"/good/spt.old-9\".to_string(),\n            candidate_started_ms: 0,\n            prior_version: None,\n        })\n    }\n    fn ready_generation(&self) -> Option<u64> {\n        // LATCH: withhold the real subprocess's readiness from run_trial until the\n        // test has armed the old-gen wedge, so the DRAINED gate has a matured wedge to\n        // block on when readiness is first seen (never a trivial no-wedge promote).\n        if !self.armed.load(Ordering::Relaxed) {\n            return None;\n        }\n        ready_generation_at(&self.ready_path)\n    }\n    fn clear_ready(&self) {\n        let _ = std::fs::remove_file(&self.ready_path);\n    }\n    fn record_promoted(&self, version: u64) {\n        self.promotions.lock().unwrap().push(version);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\n        self.rollbacks.lock().unwrap().push(quarantine);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn ready_exe_hash(&self) -> Option<String> {\n        None\n    }\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n        None\n    }\n    // The gate under drive: REAL broker truth, no brain round-trip.\n    fn old_gen_drained(&self) -> bool {\n        !self.broker.any_local_controller_wedged()\n    }\n}\n\n/// Raise `stop` (so the supervisor thread kills any live brain child and exits) and\n/// panic with `msg` — keeps a failing assertion from orphaning the subprocess brain.\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n    stop.store(true, Ordering::SeqCst);\n    panic!(\"{msg}\");\n}\n\n// [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n#[test]\nfn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    std::env::set_var(\"SPT_HOME\", home.path());\n    // Shrink the broker-side brain-write deadline so the old-gen wedge matures in\n    // ~800ms instead of 15s (read in THIS process's in-proc broker: the reap sweep AND\n    // the drained read both run here). The knob grants wall-clock only.\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"800\");\n    // Silence the subprocess brain's livehost reconcile loop (inherited by the child).\n    // livehost's own `brain.sessions()` poll (every 5s) would OTHERWISE drive the same\n    // broker reap and MASK the fix — reproducing the field's livehost-silent trial\n    // condition (PIN Q2) isolates the RED to the fix's core-loop drive. See the module\n    // doc + the gate in livehost.rs. Also set explicitly on the child spawn below.\n    std::env::set_var(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\");\n\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n\n    // ── The in-process broker (leg-b stable kernel), bound at the SPT_HOME-derived\n    //    name the real subprocess brain cold-starts against. ──\n    let broker = net_broker(&broker_socket_name(), &home.path().join(\"broker\"));\n\n    let ready_path = home.path().join(\"brain.ready\");\n    let stop = Arc::new(AtomicBool::new(false));\n    let env = Arc::new(DrainDriveEnv {\n        broker: Arc::clone(&broker),\n        ready_path: ready_path.clone(),\n        armed: AtomicBool::new(false),\n        promotions: Mutex::new(Vec::new()),\n        rollbacks: Mutex::new(Vec::new()),\n        stop: Arc::clone(&stop),\n    });\n\n    // ── Drive the REAL promotion path (supervise_brain → run_trial), spawning the\n    //    REAL `spt daemon brain` child running production run_brain (empty resume,\n    //    matching the field). The candidate self-drives sessions() every heartbeat. ──\n    let home_path = home.path().to_path_buf();\n    let stop_spawn = Arc::clone(&stop);\n    let sup = {\n        let stop_sup = Arc::clone(&stop);\n        let env_sup = Arc::clone(&env);\n        thread::spawn(move || {\n            let restart = BrainRestart::new();\n            supervise_brain(\n                &stop_sup,\n                &restart,\n                Duration::from_millis(50),\n                env_sup.as_ref(),\n                Duration::from_secs(60), // generous window — covers a slow-CI boot (eaten before the\n                                         // ready-latch arms) plus the self-drive reap, with margin\n                move |gen, reason: StartReason, binary| {\n                    if binary.is_some() {\n                        // A rollback spawn means the trial FAILED to promote (the RED\n                        // path). record_rolled_back has already raised stop, so end the\n                        // run rather than launch the bogus rollback binary.\n                        stop_spawn.store(true, Ordering::Relaxed);\n                        return placeholder_child();\n                    }\n                    Command::new(&spt_bin)\n                        .no_window()\n                        .args([\n                            \"daemon\",\n                            \"brain\",\n                            \"--generation\",\n                            &gen.to_string(),\n                            \"--start-reason\",\n                            reason.as_arg(),\n                        ])\n                        .env(\"SPT_HOME\", &home_path)\n                        // Reproduce the field's livehost-silent trial (PIN Q2): the ONLY\n                        // KIND_SESSIONS driver in the child is the fix's core heartbeat.\n                        .env(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\")\n                        .spawn()\n                },\n            );\n        })\n    };\n\n    // ── The candidate boots, resumes against an EMPTY session set (matching the\n    //    field), and stamps brain.ready. ready is still LATCHED (env.armed=false), so\n    //    run_trial has NOT promoted — it idles waiting on readiness. ──\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    // ── Establish conn A: spawn a flood session AFTER the candidate's boot-once\n    //    resume, so resume never covered it. dispatch_spawn pre-attaches A as the\n    //    session's LOCAL controller (by = None) — the old-generation-conn identity.\n    //    A then goes silent: the child's ONE burst fills A's recv buffer, A's broker-side\n    //    controller writer parks BLOCKED in write_frame (`write_blocked_since` set), and\n    //    with no further output the orthogonal 5s output-driven detach never fires — so\n    //    any_local_controller_wedged() reads true once the (shrunk) deadline matures and\n    //    PERSISTS until the reap, exactly the state a hard-killed old brain leaves its\n    //    idle-session broker conn in. ──\n    let mut a = connect(&broker_socket_name());\n    send(\n        &mut a,\n        KIND_SPAWN,\n        serde_json::to_value(burst_then_idle_spawn_req(\"update-wedge-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    // A parked long past the test; process-per-test exit reaps it.\n    thread::spawn(move || {\n        thread::sleep(Duration::from_secs(120));\n        drop(a);\n    });\n\n    // ── Confirm the wedge FORMED before arming — DETERMINISTIC (a fixed sleep races\n    //    formation: the flood child can be slow to start flooding, so conn A may not be\n    //    write-blocked yet). The wedge is proven real once EITHER the broker reads it\n    //    wedged (matured, not-yet-reaped) OR a stall-evict has fired since we spawned A\n    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\n    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\n    //    masking the very regression the RED must catch. ──\n    let baseline_evicts = read_evicts(&broker_socket_name());\n    let form_deadline = Instant::now() + Duration::from_secs(30);\n    let formed = loop {\n        if broker.any_local_controller_wedged()\n            || read_evicts(&broker_socket_name()) > baseline_evicts\n        {\n            break true;\n        }\n        if Instant::now() >= form_deadline {\n            break false;\n        }\n        thread::sleep(Duration::from_millis(50));\n    };\n    if !formed {\n        teardown_panic(\n            &stop,\n            \"PRECONDITION: the old-gen controller wedge never formed (the flood child \\\n             never write-blocked conn A) — the rig cannot exercise the DRAINED gate\",\n        );\n    }\n\n    // ── ARM: run_trial may now observe the candidate's readiness. With a matured\n    //    old-gen wedge, the DRAINED gate blocks promotion UNTIL the candidate's OWN\n    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\n    //    commented out): nothing reaps it → old_gen_drained stays false → the window\n    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\n    env.armed.store(true, Ordering::Relaxed);\n\n    // Wait out the trial verdict: GREEN promotes within a heartbeat of arming; RED\n    // (no drive, livehost silenced) never drains, so run_trial only rolls back when its\n    // window elapses — so this ceiling MUST exceed the trial window to OBSERVE the RED\n    // rollback (GREEN still exits in ~1 heartbeat, so the long ceiling costs nothing).\n    let deadline = Instant::now() + Duration::from_secs(75);\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    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\n    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\n    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\n    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\n    //    zero — the evict IS the reap. ──\n    let evicts = read_evicts(&broker_socket_name());\n\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess\n    //    brain + the flood child). stop kills the live brain child; join reaps it. ──\n    stop.store(true, Ordering::Relaxed);\n    if let Some(pid) = broker.session_pid(sid) {\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    eprintln!(\n        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\n         rollbacks={rollbacks:?} stall_evicts={evicts} \\\n         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\n    );\n\n    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\n    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n    assert!(\n        promoted,\n        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n         the window. RED (comment the production `let _ = brain.sessions();` in \\\n…\n         rollbacks={rollbacks:?}\"\n    );\n…\n}\n\n[Showing lines 1-523 of 548. Use :524 to continue]"}],"details":{"truncation":{"content":"//! UPDATE-WEDGE counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) — the A' int rig\r\n//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\r\n//! production `run_brain` self-drives the broker's controller-liveness reap so a\r\n//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\r\n//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\r\n//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\r\n//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\r\n//! operator take; here the candidate is the real subprocess whose OWN heartbeat\r\n//! `Brain::sessions()` drives the reap — the exact production line the 2026-07-09\r\n//! field freeze proved missing.\r\n//!\r\n//! ## The field freeze this locks down (root, log-anchored)\r\n//!\r\n//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\r\n//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\r\n//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\r\n//! true → the new candidate's promotion DRAINED gate (`run_trial` →\r\n//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\r\n//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\r\n//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\r\n//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\r\n//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\r\n//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\r\n//! OWN trial with no coordinated broker restart.\r\n//!\r\n//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\r\n//!\r\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\r\n//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\r\n//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\r\n//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\r\n//! only makes this rig MORE faithful; it still establishes the wedge after boot for\r\n//! continuity. The historical `resume_sessions()` framing is retained below for context.\r\n//!\r\n//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\r\n//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\r\n//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\r\n//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\r\n//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\r\n//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\r\n//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\r\n//! flood session and black-holes as its by:None controller — a session the boot-once\r\n//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\r\n//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\r\n//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\r\n//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\r\n//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\r\n//! read BROKER-side, in THIS process's in-proc broker where the reap + the drained read\r\n//! both run.\r\n//!\r\n//! ## The DRAINED gate is held open deterministically (the ready latch)\r\n//!\r\n//! The real subprocess stamps `brain.ready` autonomously the instant it boots — before\r\n//! the test can establish the wedge. So the rig's [`TrialEnv::ready_generation`]\r\n//! WITHHOLDS the candidate's readiness from `run_trial` until the test ARMS it (after\r\n//! the old-gen wedge is up + matured), then reads the REAL `brain.ready` file. This is\r\n//! the process-level analog of false_promote's scripted-ready `Mutex`: it guarantees\r\n//! the DRAINED gate has a matured wedge to block on when readiness is first seen, so\r\n//! the promote is a genuine drive-cleared-the-wedge, never a trivial no-wedge promote.\r\n//!\r\n//! ## What this rig asserts — BOTH legs are \"the brain's OWN self-drive clears it\"\r\n//!\r\n//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\r\n//!   wedge and the candidate ready, the trial PROMOTES within the window — the\r\n//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\r\n//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\r\n//!   before commit): comment out the production drive `let _ = brain.sessions();`\r\n//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\r\n//!   test rolls back instead (`WindowElapsedAlive` → the field freeze) — proving the\r\n//!   drive is load-bearing. For the RED to bite, the run_brain heartbeat must be the\r\n//!   ONLY `KIND_SESSIONS` driver: the subprocess brain's livehost reconcile loop also\r\n//!   polls `brain.sessions()` every 5s and would drive the same reap, so the rig sets\r\n//!   `SPT_LIVEHOST_RECONCILE_DISABLE` to silence it — faithfully reproducing the field's\r\n//!   livehost-silent update-trial (PIN Q2: the trial-brain livehost's first reconcile\r\n//!   tick landed around the 30s kill), the exact condition the fix makes the reap\r\n//!   independent of.**\r\n//! - **Leg 2 (lighter, doyle FINALIZE refinement).** Assert the broker's stall-evict\r\n//!   tally INCREMENTED — a reap actually FIRED via the brain's own drive (the test never\r\n//!   polls `KIND_SESSIONS` itself, so the subprocess candidate is the ONLY driver). We\r\n//!   do NOT assert zero evicts: the stall-evict IS the reap mechanism, so a\r\n//!   count==0 assertion would be asserting the fix does not work. The recovery\r\n//!   (crash-reason) brain runs the IDENTICAL `run_brain` heartbeat, so this tally —\r\n//!   confirmed by the promote — is the same-code-path evidence that a rolled-back brain\r\n//!   also self-heals its control plane promptly on its own heartbeat, not an external poll.\r\n//!\r\n//! ## RUNNER CONTRACT — single `#[test]`, run process-per-test (nextest) or -j1\r\n//!\r\n//! This is the SOLE `#[test]` here by design: it owns the process-global `SPT_HOME`\r\n//! (the subprocess brain derives `broker_socket_name()` from it, so the in-proc broker\r\n//! and the child must share ONE home — the brain_survive.rs discipline). A SECOND\r\n//! `#[test]` added here MUST bring env-lock serialization or it races this one. Run with\r\n//! `cargo nextest run --test trial_drain_drive_e2e` (CI's path) or\r\n//! `cargo test --test trial_drain_drive_e2e -- --test-threads=1`. Never a bare\r\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\r\n//!\r\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\r\n//! installer-detection heuristics demand UAC elevation for a test exe whose name\r\n//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\r\n//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\r\n\r\nuse std::path::PathBuf;\r\nuse std::process::{Child, Command};\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::{Arc, Mutex};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::brain::Brain;\r\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::endpoint::broker_socket_name;\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\r\nuse spt_daemon::nethost::{NetConfig, NetHost};\r\nuse spt_daemon::relcache::AppliedRecord;\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\r\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\r\nuse spt_proto::identity::Identity;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\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\n/// A child that emits ONE large BURST then goes IDLE (never reads stdin). The burst\r\n/// (~1 MiB, single fast write) overflows the non-reading controller conn A's socket recv\r\n/// buffer so A's broker-side writer parks BLOCKED inside `write_frame` — setting\r\n/// `write_blocked_since` (the stall-evict/reap signal). Then the child is SILENT, so no\r\n/// further output reaches the drain: the ORTHOGONAL, output-driven `CONTROLLER_WRITE_DEADLINE`\r\n/// detach (5s, tripped only by a channel that stays FULL as new chunks arrive) never fires,\r\n/// and the wedge PERSISTS until the `BRAIN_WRITE_DEADLINE` reap (the fix's heartbeat drive)\r\n/// clears it. This is the FAITHFUL analog of the field's idle-brain black-hole (broker.rs\r\n/// BRAIN_WRITE_DEADLINE doc): a suspended brain conn with a stuck write on an idle session.\r\n/// A CONTINUOUS flood (false_promote.rs) instead trips the 5s output-driven detach and only\r\n/// works because that rig finishes < 5s — the slower subprocess rig would self-heal.\r\nfn burst_then_idle_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\r\n            \"-c\".to_string(),\r\n            \"yes FLOODFLOODFLOODFLOOD | head -c 1048576; sleep 600\".to_string(),\r\n        ],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"powershell\".to_string(),\r\n        vec![\r\n            \"-NoProfile\".to_string(),\r\n            \"-NonInteractive\".to_string(),\r\n            \"-Command\".to_string(),\r\n            \"[Console]::Out.Write('F' * 1048576); Start-Sleep -Seconds 600\".to_string(),\r\n        ],\r\n    );\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/// A harmless short-lived child for the rollback spawn slot (never reached on the\r\n/// GREEN promote path; on a RED rollback `record_rolled_back` has already raised\r\n/// `stop`, so the supervisor exits before it runs — this only satisfies the\r\n/// closure's `io::Result<Child>` return).\r\nfn placeholder_child() -> std::io::Result<Child> {\r\n    #[cfg(windows)]\r\n    {\r\n        Command::new(\"cmd\")\r\n            .no_window()\r\n            .args([\"/C\", \"ping\", \"-n\", \"3\", \"127.0.0.1\"])\r\n            .stdout(std::process::Stdio::null())\r\n            .spawn()\r\n    }\r\n    #[cfg(unix)]\r\n    {\r\n        Command::new(\"sleep\").arg(\"2\").spawn()\r\n    }\r\n}\r\n\r\n/// A hermetic net config (mirrors brain_survive.rs / netbroker.rs): relays disabled,\r\n/// no discovery, loopback-only — no real interface touched, no firewall prompt. Net is\r\n/// enabled so the real subprocess brain boots the proven way (the field brain had net up\r\n/// too). The subprocess's aux `KIND_SESSIONS` driver — livehost's reconcile poll — is\r\n/// silenced via `SPT_LIVEHOST_RECONCILE_DISABLE` (see the module doc), so the ONLY reap\r\n/// driver left is the fix's core heartbeat, and the RED stays honest.\r\nfn hermetic(identity: Identity) -> NetConfig {\r\n    NetConfig {\r\n        identity,\r\n        relay: RelayPolicy::Disabled,\r\n        discovery: LocalDiscovery::Off,\r\n        scope: BindScope::Loopback,\r\n        membership: None,\r\n    }\r\n}\r\n\r\n/// Bind a net-enabled broker (serving on a background thread) at `name`, effect\r\n/// journal under `dir`.\r\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\r\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\r\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\r\n        .expect(\"bind broker with net\");\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 send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\r\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\r\n}\r\n\r\n/// Connect a raw brain conn to the broker (retrying while it finishes binding).\r\nfn connect(name: &str) -> impl std::io::Read + std::io::Write {\r\n    for _ in 0..300 {\r\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\r\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\r\n            return c;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"could not connect to broker {name}\");\r\n}\r\n\r\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + resumed + stamped\r\n/// ready), returning that pid. `None` if it never appears in `budget`.\r\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some(pid) = std::fs::read_to_string(path)\r\n            .ok()\r\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\r\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\r\n        {\r\n            return Some(pid as u32);\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\n/// The broker's cumulative stall-evict tally via `KIND_STALL_EVICTS` (a PURE tally read —\r\n/// it does NOT drive a reap, unlike a `KIND_SESSIONS` poll). `0` if unreachable/unreported.\r\nfn read_evicts(name: &str) -> u64 {\r\n    Brain::cold_start(name, 1)\r\n        .ok()\r\n        .and_then(|mut b| b.stall_evicts().ok().flatten())\r\n        .map(|(count, _last_ms)| count)\r\n        .unwrap_or(0)\r\n}\r\n\r\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads\r\n/// REAL broker truth (`any_local_controller_wedged`) off the live in-proc broker (the\r\n/// whole point), and whose readiness is LATCHED behind `armed` — withheld from\r\n/// `run_trial` until the test has established the old-gen wedge, then read from the\r\n/// REAL `brain.ready` file the subprocess wrote. `record_promoted` / `record_rolled_back`\r\n/// capture the verdict AND raise `stop` so `supervise_brain` returns.\r\nstruct DrainDriveEnv {\r\n    broker: Arc<Broker>,\r\n    ready_path: PathBuf,\r\n    armed: AtomicBool,\r\n    promotions: Mutex<Vec<u64>>,\r\n    rollbacks: Mutex<Vec<u64>>,\r\n    stop: Arc<AtomicBool>,\r\n}\r\n\r\nimpl TrialEnv for DrainDriveEnv {\r\n    fn applied_state(&self) -> Option<AppliedRecord> {\r\n        Some(AppliedRecord::AppliedPending {\r\n            version: 9,\r\n            rollback_binary: \"/good/spt.old-9\".to_string(),\r\n            candidate_started_ms: 0,\r\n            prior_version: None,\r\n        })\r\n    }\r\n    fn ready_generation(&self) -> Option<u64> {\r\n        // LATCH: withhold the real subprocess's readiness from run_trial until the\r\n        // test has armed the old-gen wedge, so the DRAINED gate has a matured wedge to\r\n        // block on when readiness is first seen (never a trivial no-wedge promote).\r\n        if !self.armed.load(Ordering::Relaxed) {\r\n            return None;\r\n        }\r\n        ready_generation_at(&self.ready_path)\r\n    }\r\n    fn clear_ready(&self) {\r\n        let _ = std::fs::remove_file(&self.ready_path);\r\n    }\r\n    fn record_promoted(&self, version: u64) {\r\n        self.promotions.lock().unwrap().push(version);\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\r\n        self.rollbacks.lock().unwrap().push(quarantine);\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n    fn ready_exe_hash(&self) -> Option<String> {\r\n        None\r\n    }\r\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\r\n        None\r\n    }\r\n    // The gate under drive: REAL broker truth, no brain round-trip.\r\n    fn old_gen_drained(&self) -> bool {\r\n        !self.broker.any_local_controller_wedged()\r\n    }\r\n}\r\n\r\n/// Raise `stop` (so the supervisor thread kills any live brain child and exits) and\r\n/// panic with `msg` — keeps a failing assertion from orphaning the subprocess brain.\r\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\r\n    stop.store(true, Ordering::SeqCst);\r\n    panic!(\"{msg}\");\r\n}\r\n\r\n// [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n#[test]\r\nfn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n    // Shrink the broker-side brain-write deadline so the old-gen wedge matures in\r\n    // ~800ms instead of 15s (read in THIS process's in-proc broker: the reap sweep AND\r\n    // the drained read both run here). The knob grants wall-clock only.\r\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"800\");\r\n    // Silence the subprocess brain's livehost reconcile loop (inherited by the child).\r\n    // livehost's own `brain.sessions()` poll (every 5s) would OTHERWISE drive the same\r\n    // broker reap and MASK the fix — reproducing the field's livehost-silent trial\r\n    // condition (PIN Q2) isolates the RED to the fix's core-loop drive. See the module\r\n    // doc + the gate in livehost.rs. Also set explicitly on the child spawn below.\r\n    std::env::set_var(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\");\r\n\r\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n\r\n    // ── The in-process broker (leg-b stable kernel), bound at the SPT_HOME-derived\r\n    //    name the real subprocess brain cold-starts against. ──\r\n    let broker = net_broker(&broker_socket_name(), &home.path().join(\"broker\"));\r\n\r\n    let ready_path = home.path().join(\"brain.ready\");\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let env = Arc::new(DrainDriveEnv {\r\n        broker: Arc::clone(&broker),\r\n        ready_path: ready_path.clone(),\r\n        armed: AtomicBool::new(false),\r\n        promotions: Mutex::new(Vec::new()),\r\n        rollbacks: Mutex::new(Vec::new()),\r\n        stop: Arc::clone(&stop),\r\n    });\r\n\r\n    // ── Drive the REAL promotion path (supervise_brain → run_trial), spawning the\r\n    //    REAL `spt daemon brain` child running production run_brain (empty resume,\r\n    //    matching the field). The candidate self-drives sessions() every heartbeat. ──\r\n    let home_path = home.path().to_path_buf();\r\n    let stop_spawn = Arc::clone(&stop);\r\n    let sup = {\r\n        let stop_sup = Arc::clone(&stop);\r\n        let env_sup = Arc::clone(&env);\r\n        thread::spawn(move || {\r\n            let restart = BrainRestart::new();\r\n            supervise_brain(\r\n                &stop_sup,\r\n                &restart,\r\n                Duration::from_millis(50),\r\n                env_sup.as_ref(),\r\n                Duration::from_secs(60), // generous window — covers a slow-CI boot (eaten before the\r\n                                         // ready-latch arms) plus the self-drive reap, with margin\r\n                move |gen, reason: StartReason, binary| {\r\n                    if binary.is_some() {\r\n                        // A rollback spawn means the trial FAILED to promote (the RED\r\n                        // path). record_rolled_back has already raised stop, so end the\r\n                        // run rather than launch the bogus rollback binary.\r\n                        stop_spawn.store(true, Ordering::Relaxed);\r\n                        return placeholder_child();\r\n                    }\r\n                    Command::new(&spt_bin)\r\n                        .no_window()\r\n                        .args([\r\n                            \"daemon\",\r\n                            \"brain\",\r\n                            \"--generation\",\r\n                            &gen.to_string(),\r\n                            \"--start-reason\",\r\n                            reason.as_arg(),\r\n                        ])\r\n                        .env(\"SPT_HOME\", &home_path)\r\n                        // Reproduce the field's livehost-silent trial (PIN Q2): the ONLY\r\n                        // KIND_SESSIONS driver in the child is the fix's core heartbeat.\r\n                        .env(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\")\r\n                        .spawn()\r\n                },\r\n            );\r\n        })\r\n    };\r\n\r\n    // ── The candidate boots, resumes against an EMPTY session set (matching the\r\n    //    field), and stamps brain.ready. ready is still LATCHED (env.armed=false), so\r\n    //    run_trial has NOT promoted — it idles waiting on readiness. ──\r\n    let _cand_pid = wait_ready_pid(&ready_path, Duration::from_secs(45))\r\n        .unwrap_or_else(|| teardown_panic(&stop, \"the trial candidate never stamped brain.ready\"));\r\n\r\n    // ── Establish conn A: spawn a flood session AFTER the candidate's boot-once\r\n    //    resume, so resume never covered it. dispatch_spawn pre-attaches A as the\r\n    //    session's LOCAL controller (by = None) — the old-generation-conn identity.\r\n    //    A then goes silent: the child's ONE burst fills A's recv buffer, A's broker-side\r\n    //    controller writer parks BLOCKED in write_frame (`write_blocked_since` set), and\r\n    //    with no further output the orthogonal 5s output-driven detach never fires — so\r\n    //    any_local_controller_wedged() reads true once the (shrunk) deadline matures and\r\n    //    PERSISTS until the reap, exactly the state a hard-killed old brain leaves its\r\n    //    idle-session broker conn in. ──\r\n    let mut a = connect(&broker_socket_name());\r\n    send(\r\n        &mut a,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(burst_then_idle_spawn_req(\"update-wedge-ep\")).unwrap(),\r\n    );\r\n    let sid = loop {\r\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\r\n        }\r\n    };\r\n    // A parked long past the test; process-per-test exit reaps it.\r\n    thread::spawn(move || {\r\n        thread::sleep(Duration::from_secs(120));\r\n        drop(a);\r\n    });\r\n\r\n    // ── Confirm the wedge FORMED before arming — DETERMINISTIC (a fixed sleep races\r\n    //    formation: the flood child can be slow to start flooding, so conn A may not be\r\n    //    write-blocked yet). The wedge is proven real once EITHER the broker reads it\r\n    //    wedged (matured, not-yet-reaped) OR a stall-evict has fired since we spawned A\r\n    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\r\n    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\r\n    //    masking the very regression the RED must catch. ──\r\n    let baseline_evicts = read_evicts(&broker_socket_name());\r\n    let form_deadline = Instant::now() + Duration::from_secs(30);\r\n    let formed = loop {\r\n        if broker.any_local_controller_wedged()\r\n            || read_evicts(&broker_socket_name()) > baseline_evicts\r\n        {\r\n            break true;\r\n        }\r\n        if Instant::now() >= form_deadline {\r\n            break false;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    };\r\n    if !formed {\r\n        teardown_panic(\r\n            &stop,\r\n            \"PRECONDITION: the old-gen controller wedge never formed (the flood child \\\r\n             never write-blocked conn A) — the rig cannot exercise the DRAINED gate\",\r\n        );\r\n    }\r\n\r\n    // ── ARM: run_trial may now observe the candidate's readiness. With a matured\r\n    //    old-gen wedge, the DRAINED gate blocks promotion UNTIL the candidate's OWN\r\n    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\r\n    //    commented out): nothing reaps it → old_gen_drained stays false → the window\r\n    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\r\n    env.armed.store(true, Ordering::Relaxed);\r\n\r\n    // Wait out the trial verdict: GREEN promotes within a heartbeat of arming; RED\r\n    // (no drive, livehost silenced) never drains, so run_trial only rolls back when its\r\n    // window elapses — so this ceiling MUST exceed the trial window to OBSERVE the RED\r\n    // rollback (GREEN still exits in ~1 heartbeat, so the long ceiling costs nothing).\r\n    let deadline = Instant::now() + Duration::from_secs(75);\r\n    let promoted = loop {\r\n        if !env.promotions.lock().unwrap().is_empty() {\r\n            break true;\r\n        }\r\n        if !env.rollbacks.lock().unwrap().is_empty() || Instant::now() >= deadline {\r\n            break false;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    };\r\n\r\n    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\r\n    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\r\n    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\r\n    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\r\n    //    zero — the evict IS the reap. ──\r\n    let evicts = read_evicts(&broker_socket_name());\r\n\r\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess\r\n    //    brain + the flood child). stop kills the live brain child; join reaps it. ──\r\n    stop.store(true, Ordering::Relaxed);\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n    let _ = sup.join();\r\n\r\n    let promotions = env.promotions.lock().unwrap().clone();\r\n    let rollbacks = env.rollbacks.lock().unwrap().clone();\r\n    eprintln!(\r\n        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\r\n         rollbacks={rollbacks:?} stall_evicts={evicts} \\\r\n         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\r\n    );\r\n\r\n    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\r\n    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\r\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n    assert!(\r\n        promoted,\r\n        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\r\n         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\r\n         the window. RED (comment the production `let _ = brain.sessions();` in \\\r","truncated":true,"truncatedBy":"lines","totalLines":548,"totalBytes":26565,"outputLines":523,"outputBytes":26565,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! UPDATE-WEDGE counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) — the A' int rig\r\n//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\r\n//! production `run_brain` self-drives the broker's controller-liveness reap so a\r\n//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\r\n//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\r\n//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\r\n//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\r\n//! operator take; here the candidate is the real subprocess whose OWN heartbeat\r\n//! `Brain::sessions()` drives the reap — the exact production line the 2026-07-09\r\n//! field freeze proved missing.\r\n//!\r\n//! ## The field freeze this locks down (root, log-anchored)\r\n//!\r\n//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\r\n//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\r\n//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\r\n//! true → the new candidate's promotion DRAINED gate (`run_trial` →\r\n//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\r\n//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\r\n//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\r\n//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\r\n//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\r\n//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\r\n//! OWN trial with no coordinated broker restart.\r\n//!\r\n//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\r\n//!\r\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\r\n//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\r\n//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\r\n//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\r\n//! only makes this rig MORE faithful; it still establishes the wedge after boot for\r\n//! continuity. The historical `resume_sessions()` framing is retained below for context.\r\n//!\r\n//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\r\n//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\r\n//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\r\n//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\r\n//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\r\n//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\r\n//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\r\n//! flood session and black-holes as its by:None controller — a session the boot-once\r\n//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\r\n//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\r\n//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\r\n//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\r\n//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\r\n//! read BROKER-side, in THIS process's in-proc broker where the reap + the drained read\r\n//! both run.\r\n//!\r\n//! ## The DRAINED gate is held open deterministically (the ready latch)\r\n//!\r\n//! The real subprocess stamps `brain.ready` autonomously the instant it boots — before\r\n//! the test can establish the wedge. So the rig's [`TrialEnv::ready_generation`]\r\n//! WITHHOLDS the candidate's readiness from `run_trial` until the test ARMS it (after\r\n//! the old-gen wedge is up + matured), then reads the REAL `brain.ready` file. This is\r\n//! the process-level analog of false_promote's scripted-ready `Mutex`: it guarantees\r\n//! the DRAINED gate has a matured wedge to block on when readiness is first seen, so\r\n//! the promote is a genuine drive-cleared-the-wedge, never a trivial no-wedge promote.\r\n//!\r\n//! ## What this rig asserts — BOTH legs are \"the brain's OWN self-drive clears it\"\r\n//!\r\n//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\r\n//!   wedge and the candidate ready, the trial PROMOTES within the window — the\r\n//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\r\n//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\r\n//!   before commit): comment out the production drive `let _ = brain.sessions();`\r\n//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\r\n//!   test rolls back instead (`WindowElapsedAlive` → the field freeze) — proving the\r\n//!   drive is load-bearing. For the RED to bite, the run_brain heartbeat must be the\r\n//!   ONLY `KIND_SESSIONS` driver: the subprocess brain's livehost reconcile loop also\r\n//!   polls `brain.sessions()` every 5s and would drive the same reap, so the rig sets\r\n//!   `SPT_LIVEHOST_RECONCILE_DISABLE` to silence it — faithfully reproducing the field's\r\n//!   livehost-silent update-trial (PIN Q2: the trial-brain livehost's first reconcile\r\n//!   tick landed around the 30s kill), the exact condition the fix makes the reap\r\n//!   independent of.**\r\n//! - **Leg 2 (lighter, doyle FINALIZE refinement).** Assert the broker's stall-evict\r\n//!   tally INCREMENTED — a reap actually FIRED via the brain's own drive (the test never\r\n//!   polls `KIND_SESSIONS` itself, so the subprocess candidate is the ONLY driver). We\r\n//!   do NOT assert zero evicts: the stall-evict IS the reap mechanism, so a\r\n//!   count==0 assertion would be asserting the fix does not work. The recovery\r\n//!   (crash-reason) brain runs the IDENTICAL `run_brain` heartbeat, so this tally —\r\n//!   confirmed by the promote — is the same-code-path evidence that a rolled-back brain\r\n//!   also self-heals its control plane promptly on its own heartbeat, not an external poll.\r\n//!\r\n//! ## RUNNER CONTRACT — single `#[test]`, run process-per-test (nextest) or -j1\r\n//!\r\n//! This is the SOLE `#[test]` here by design: it owns the process-global `SPT_HOME`\r\n//! (the subprocess brain derives `broker_socket_name()` from it, so the in-proc broker\r\n//! and the child must share ONE home — the brain_survive.rs discipline). A SECOND\r\n//! `#[test]` added here MUST bring env-lock serialization or it races this one. Run with\r\n//! `cargo nextest run --test trial_drain_drive_e2e` (CI's path) or\r\n//! `cargo test --test trial_drain_drive_e2e -- --test-threads=1`. Never a bare\r\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\r\n//!\r\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\r\n//! installer-detection heuristics demand UAC elevation for a test exe whose name\r\n//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\r\n//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\r\n\r\nuse std::path::PathBuf;\r\nuse std::process::{Child, Command};\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::{Arc, Mutex};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::brain::Brain;\r\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::endpoint::broker_socket_name;\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\r\nuse spt_daemon::nethost::{NetConfig, NetHost};\r\nuse spt_daemon::relcache::AppliedRecord;\r\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\r\nuse spt_daemon::Broker;\r\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\r\nuse spt_proto::identity::Identity;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\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\n/// A child that emits ONE large BURST then goes IDLE (never reads stdin). The burst\r\n/// (~1 MiB, single fast write) overflows the non-reading controller conn A's socket recv\r\n/// buffer so A's broker-side writer parks BLOCKED inside `write_frame` — setting\r\n/// `write_blocked_since` (the stall-evict/reap signal). Then the child is SILENT, so no\r\n/// further output reaches the drain: the ORTHOGONAL, output-driven `CONTROLLER_WRITE_DEADLINE`\r\n/// detach (5s, tripped only by a channel that stays FULL as new chunks arrive) never fires,\r\n/// and the wedge PERSISTS until the `BRAIN_WRITE_DEADLINE` reap (the fix's heartbeat drive)\r\n/// clears it. This is the FAITHFUL analog of the field's idle-brain black-hole (broker.rs\r\n/// BRAIN_WRITE_DEADLINE doc): a suspended brain conn with a stuck write on an idle session.\r\n/// A CONTINUOUS flood (false_promote.rs) instead trips the 5s output-driven detach and only\r\n/// works because that rig finishes < 5s — the slower subprocess rig would self-heal.\r\nfn burst_then_idle_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\r\n            \"-c\".to_string(),\r\n            \"yes FLOODFLOODFLOODFLOOD | head -c 1048576; sleep 600\".to_string(),\r\n        ],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"powershell\".to_string(),\r\n        vec![\r\n            \"-NoProfile\".to_string(),\r\n            \"-NonInteractive\".to_string(),\r\n            \"-Command\".to_string(),\r\n            \"[Console]::Out.Write('F' * 1048576); Start-Sleep -Seconds 600\".to_string(),\r\n        ],\r\n    );\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/// A harmless short-lived child for the rollback spawn slot (never reached on the\r\n/// GREEN promote path; on a RED rollback `record_rolled_back` has already raised\r\n/// `stop`, so the supervisor exits before it runs — this only satisfies the\r\n/// closure's `io::Result<Child>` return).\r\nfn placeholder_child() -> std::io::Result<Child> {\r\n    #[cfg(windows)]\r\n    {\r\n        Command::new(\"cmd\")\r\n            .no_window()\r\n            .args([\"/C\", \"ping\", \"-n\", \"3\", \"127.0.0.1\"])\r\n            .stdout(std::process::Stdio::null())\r\n            .spawn()\r\n    }\r\n    #[cfg(unix)]\r\n    {\r\n        Command::new(\"sleep\").arg(\"2\").spawn()\r\n    }\r\n}\r\n\r\n/// A hermetic net config (mirrors brain_survive.rs / netbroker.rs): relays disabled,\r\n/// no discovery, loopback-only — no real interface touched, no firewall prompt. Net is\r\n/// enabled so the real subprocess brain boots the proven way (the field brain had net up\r\n/// too). The subprocess's aux `KIND_SESSIONS` driver — livehost's reconcile poll — is\r\n/// silenced via `SPT_LIVEHOST_RECONCILE_DISABLE` (see the module doc), so the ONLY reap\r\n/// driver left is the fix's core heartbeat, and the RED stays honest.\r\nfn hermetic(identity: Identity) -> NetConfig {\r\n    NetConfig {\r\n        identity,\r\n        relay: RelayPolicy::Disabled,\r\n        discovery: LocalDiscovery::Off,\r\n        scope: BindScope::Loopback,\r\n        membership: None,\r\n    }\r\n}\r\n\r\n/// Bind a net-enabled broker (serving on a background thread) at `name`, effect\r\n/// journal under `dir`.\r\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\r\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\r\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\r\n        .expect(\"bind broker with net\");\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 send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\r\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\r\n}\r\n\r\n/// Connect a raw brain conn to the broker (retrying while it finishes binding).\r\nfn connect(name: &str) -> impl std::io::Read + std::io::Write {\r\n    for _ in 0..300 {\r\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\r\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\r\n            return c;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"could not connect to broker {name}\");\r\n}\r\n\r\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + resumed + stamped\r\n/// ready), returning that pid. `None` if it never appears in `budget`.\r\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some(pid) = std::fs::read_to_string(path)\r\n            .ok()\r\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\r\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\r\n        {\r\n            return Some(pid as u32);\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\n/// The broker's cumulative stall-evict tally via `KIND_STALL_EVICTS` (a PURE tally read —\r\n/// it does NOT drive a reap, unlike a `KIND_SESSIONS` poll). `0` if unreachable/unreported.\r\nfn read_evicts(name: &str) -> u64 {\r\n    Brain::cold_start(name, 1)\r\n        .ok()\r\n        .and_then(|mut b| b.stall_evicts().ok().flatten())\r\n        .map(|(count, _last_ms)| count)\r\n        .unwrap_or(0)\r\n}\r\n\r\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads\r\n/// REAL broker truth (`any_local_controller_wedged`) off the live in-proc broker (the\r\n/// whole point), and whose readiness is LATCHED behind `armed` — withheld from\r\n/// `run_trial` until the test has established the old-gen wedge, then read from the\r\n/// REAL `brain.ready` file the subprocess wrote. `record_promoted` / `record_rolled_back`\r\n/// capture the verdict AND raise `stop` so `supervise_brain` returns.\r\nstruct DrainDriveEnv {\r\n    broker: Arc<Broker>,\r\n    ready_path: PathBuf,\r\n    armed: AtomicBool,\r\n    promotions: Mutex<Vec<u64>>,\r\n    rollbacks: Mutex<Vec<u64>>,\r\n    stop: Arc<AtomicBool>,\r\n}\r\n\r\nimpl TrialEnv for DrainDriveEnv {\r\n    fn applied_state(&self) -> Option<AppliedRecord> {\r\n        Some(AppliedRecord::AppliedPending {\r\n            version: 9,\r\n            rollback_binary: \"/good/spt.old-9\".to_string(),\r\n            candidate_started_ms: 0,\r\n            prior_version: None,\r\n        })\r\n    }\r\n    fn ready_generation(&self) -> Option<u64> {\r\n        // LATCH: withhold the real subprocess's readiness from run_trial until the\r\n        // test has armed the old-gen wedge, so the DRAINED gate has a matured wedge to\r\n        // block on when readiness is first seen (never a trivial no-wedge promote).\r\n        if !self.armed.load(Ordering::Relaxed) {\r\n            return None;\r\n        }\r\n        ready_generation_at(&self.ready_path)\r\n    }\r\n    fn clear_ready(&self) {\r\n        let _ = std::fs::remove_file(&self.ready_path);\r\n    }\r\n    fn record_promoted(&self, version: u64) {\r\n        self.promotions.lock().unwrap().push(version);\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\r\n        self.rollbacks.lock().unwrap().push(quarantine);\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n    fn ready_exe_hash(&self) -> Option<String> {\r\n        None\r\n    }\r\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\r\n        None\r\n    }\r\n    // The gate under drive: REAL broker truth, no brain round-trip.\r\n    fn old_gen_drained(&self) -> bool {\r\n        !self.broker.any_local_controller_wedged()\r\n    }\r\n}\r\n\r\n/// Raise `stop` (so the supervisor thread kills any live brain child and exits) and\r\n/// panic with `msg` — keeps a failing assertion from orphaning the subprocess brain.\r\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\r\n    stop.store(true, Ordering::SeqCst);\r\n    panic!(\"{msg}\");\r\n}\r\n\r\n// [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n#[test]\r\nfn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n    // Shrink the broker-side brain-write deadline so the old-gen wedge matures in\r\n    // ~800ms instead of 15s (read in THIS process's in-proc broker: the reap sweep AND\r\n    // the drained read both run here). The knob grants wall-clock only.\r\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"800\");\r\n    // Silence the subprocess brain's livehost reconcile loop (inherited by the child).\r\n    // livehost's own `brain.sessions()` poll (every 5s) would OTHERWISE drive the same\r\n    // broker reap and MASK the fix — reproducing the field's livehost-silent trial\r\n    // condition (PIN Q2) isolates the RED to the fix's core-loop drive. See the module\r\n    // doc + the gate in livehost.rs. Also set explicitly on the child spawn below.\r\n    std::env::set_var(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\");\r\n\r\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n\r\n    // ── The in-process broker (leg-b stable kernel), bound at the SPT_HOME-derived\r\n    //    name the real subprocess brain cold-starts against. ──\r\n    let broker = net_broker(&broker_socket_name(), &home.path().join(\"broker\"));\r\n\r\n    let ready_path = home.path().join(\"brain.ready\");\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let env = Arc::new(DrainDriveEnv {\r\n        broker: Arc::clone(&broker),\r\n        ready_path: ready_path.clone(),\r\n        armed: AtomicBool::new(false),\r\n        promotions: Mutex::new(Vec::new()),\r\n        rollbacks: Mutex::new(Vec::new()),\r\n        stop: Arc::clone(&stop),\r\n    });\r\n\r\n    // ── Drive the REAL promotion path (supervise_brain → run_trial), spawning the\r\n    //    REAL `spt daemon brain` child running production run_brain (empty resume,\r\n    //    matching the field). The candidate self-drives sessions() every heartbeat. ──\r\n    let home_path = home.path().to_path_buf();\r\n    let stop_spawn = Arc::clone(&stop);\r\n    let sup = {\r\n        let stop_sup = Arc::clone(&stop);\r\n        let env_sup = Arc::clone(&env);\r\n        thread::spawn(move || {\r\n            let restart = BrainRestart::new();\r\n            supervise_brain(\r\n                &stop_sup,\r\n                &restart,\r\n                Duration::from_millis(50),\r\n                env_sup.as_ref(),\r\n                Duration::from_secs(60), // generous window — covers a slow-CI boot (eaten before the\r\n                                         // ready-latch arms) plus the self-drive reap, with margin\r\n                move |gen, reason: StartReason, binary| {\r\n                    if binary.is_some() {\r\n                        // A rollback spawn means the trial FAILED to promote (the RED\r\n                        // path). record_rolled_back has already raised stop, so end the\r\n                        // run rather than launch the bogus rollback binary.\r\n                        stop_spawn.store(true, Ordering::Relaxed);\r\n                        return placeholder_child();\r\n                    }\r\n                    Command::new(&spt_bin)\r\n                        .no_window()\r\n                        .args([\r\n                            \"daemon\",\r\n                            \"brain\",\r\n                            \"--generation\",\r\n                            &gen.to_string(),\r\n                            \"--start-reason\",\r\n                            reason.as_arg(),\r\n                        ])\r\n                        .env(\"SPT_HOME\", &home_path)\r\n                        // Reproduce the field's livehost-silent trial (PIN Q2): the ONLY\r\n                        // KIND_SESSIONS driver in the child is the fix's core heartbeat.\r\n                        .env(\"SPT_LIVEHOST_RECONCILE_DISABLE\", \"1\")\r\n                        .spawn()\r\n                },\r\n            );\r\n        })\r\n    };\r\n\r\n    // ── The candidate boots, resumes against an EMPTY session set (matching the\r\n    //    field), and stamps brain.ready. ready is still LATCHED (env.armed=false), so\r\n    //    run_trial has NOT promoted — it idles waiting on readiness. ──\r\n    let _cand_pid = wait_ready_pid(&ready_path, Duration::from_secs(45))\r\n        .unwrap_or_else(|| teardown_panic(&stop, \"the trial candidate never stamped brain.ready\"));\r\n\r\n    // ── Establish conn A: spawn a flood session AFTER the candidate's boot-once\r\n    //    resume, so resume never covered it. dispatch_spawn pre-attaches A as the\r\n    //    session's LOCAL controller (by = None) — the old-generation-conn identity.\r\n    //    A then goes silent: the child's ONE burst fills A's recv buffer, A's broker-side\r\n    //    controller writer parks BLOCKED in write_frame (`write_blocked_since` set), and\r\n    //    with no further output the orthogonal 5s output-driven detach never fires — so\r\n    //    any_local_controller_wedged() reads true once the (shrunk) deadline matures and\r\n    //    PERSISTS until the reap, exactly the state a hard-killed old brain leaves its\r\n    //    idle-session broker conn in. ──\r\n    let mut a = connect(&broker_socket_name());\r\n    send(\r\n        &mut a,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(burst_then_idle_spawn_req(\"update-wedge-ep\")).unwrap(),\r\n    );\r\n    let sid = loop {\r\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\r\n        }\r\n    };\r\n    // A parked long past the test; process-per-test exit reaps it.\r\n    thread::spawn(move || {\r\n        thread::sleep(Duration::from_secs(120));\r\n        drop(a);\r\n    });\r\n\r\n    // ── Confirm the wedge FORMED before arming — DETERMINISTIC (a fixed sleep races\r\n    //    formation: the flood child can be slow to start flooding, so conn A may not be\r\n    //    write-blocked yet). The wedge is proven real once EITHER the broker reads it\r\n    //    wedged (matured, not-yet-reaped) OR a stall-evict has fired since we spawned A\r\n    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\r\n    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\r\n    //    masking the very regression the RED must catch. ──\r\n    let baseline_evicts = read_evicts(&broker_socket_name());\r\n    let form_deadline = Instant::now() + Duration::from_secs(30);\r\n    let formed = loop {\r\n        if broker.any_local_controller_wedged()\r\n            || read_evicts(&broker_socket_name()) > baseline_evicts\r\n        {\r\n            break true;\r\n        }\r\n        if Instant::now() >= form_deadline {\r\n            break false;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    };\r\n    if !formed {\r\n        teardown_panic(\r\n            &stop,\r\n            \"PRECONDITION: the old-gen controller wedge never formed (the flood child \\\r\n             never write-blocked conn A) — the rig cannot exercise the DRAINED gate\",\r\n        );\r\n    }\r\n\r\n    // ── ARM: run_trial may now observe the candidate's readiness. With a matured\r\n    //    old-gen wedge, the DRAINED gate blocks promotion UNTIL the candidate's OWN\r\n    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\r\n    //    commented out): nothing reaps it → old_gen_drained stays false → the window\r\n    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\r\n    env.armed.store(true, Ordering::Relaxed);\r\n\r\n    // Wait out the trial verdict: GREEN promotes within a heartbeat of arming; RED\r\n    // (no drive, livehost silenced) never drains, so run_trial only rolls back when its\r\n    // window elapses — so this ceiling MUST exceed the trial window to OBSERVE the RED\r\n    // rollback (GREEN still exits in ~1 heartbeat, so the long ceiling costs nothing).\r\n    let deadline = Instant::now() + Duration::from_secs(75);\r\n    let promoted = loop {\r\n        if !env.promotions.lock().unwrap().is_empty() {\r\n            break true;\r\n        }\r\n        if !env.rollbacks.lock().unwrap().is_empty() || Instant::now() >= deadline {\r\n            break false;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    };\r\n\r\n    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\r\n    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\r\n    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\r\n    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\r\n    //    zero — the evict IS the reap. ──\r\n    let evicts = read_evicts(&broker_socket_name());\r\n\r\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess\r\n    //    brain + the flood child). stop kills the live brain child; join reaps it. ──\r\n    stop.store(true, Ordering::Relaxed);\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n    let _ = sup.join();\r\n\r\n    let promotions = env.promotions.lock().unwrap().clone();\r\n    let rollbacks = env.rollbacks.lock().unwrap().clone();\r\n    eprintln!(\r\n        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\r\n         rollbacks={rollbacks:?} stall_evicts={evicts} \\\r\n         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\r\n    );\r\n\r\n    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\r\n    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\r\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n    assert!(\r\n        promoted,\r\n        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\r\n         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\r\n         the window. RED (comment the production `let _ = brain.sessions();` in \\\r\n…\n         rollbacks={rollbacks:?}\"\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,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,526,527,null,547]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\trial_drain_drive_e2e.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":548,"totalBytes":26565,"outputLines":523,"outputBytes":26565,"shownRange":{"start":1,"end":523},"nextOffset":524}}},"isError":false,"timestamp":1784195802112}}
{"type":"message","id":"274de28e","parentId":"7d84e0b4","timestamp":"2026-07-16T09:56:42.112Z","message":{"role":"toolResult","toolCallId":"call_NRaLalZtsbvuBXMgnoFufk3U|fc_0b42584a8ab0545d016a58aada06e88199b82eecac2fe4efc4","toolName":"glob","content":[{"type":"text","text":"No files found matching pattern"}],"details":{"scopePath":"crates/**/*mock*session*.rs, crates/**/mock-adapter/**, crates/**/mock_session.rs","fileCount":0,"files":[],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"useless":true,"timestamp":1784195802112}}
{"type":"message","id":"10ac5ad2","parentId":"274de28e","timestamp":"2026-07-16T09:56:42.116Z","message":{"role":"toolResult","toolCallId":"call_zdgcOI4DrAgHherfQuXpdS41|fc_0b42584a8ab0545d016a58aada06f08199bc9571368b30b14f","toolName":"grep","content":[{"type":"text","text":"# crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 35|fn send_bounded(spt_bin: &std::path::Path, home: &std::path::Path, args: &[&str], body: &str) -> Output {\n*36|    let mut child = Command::new(spt_bin)\n 37|        .no_window()\n 38|        .args(args)\n 39|        .env(\"SPT_HOME\", home)\n...\n 48|        .unwrap()\n*49|        .write_all(body.as_bytes())\n 50|        .expect(\"write stdin body\");\n 51|    let (tx, rx) = std::sync::mpsc::channel();\n 52|    std::thread::spawn(move || {\n## adapter_post_step.rs\n 28|fn run_update(spt_bin: &Path, home: &Path, mode: &str, seam_out: &Path) -> std::process::Output {\n*29|    Command::new(spt_bin)\n 30|        .no_window()\n 31|        .args([\"adapter\", \"update\", \"cc\"])\n 32|        .env(\"SPT_HOME\", home)\n## attach_wedge_e2e.rs\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 90|        .env(\"SPT_HOME\", home);\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 125|        .env(\"SPT_HOME\", home)\n 126|        .stdout(Stdio::piped())\n 127|        .stderr(Stdio::null())\n...\n 130|        .expect(\"spawn spt rc <id>\");\n*131|    let _rc_stdin = rc.stdin.take();\n*132|    let mut rc_out = rc.stdout.take().unwrap();\n 133|    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 134|    let reader = std::thread::spawn(move || {\n 135|        let mut buf = [0u8; 4096];\n...\n 207|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*208|    let mut broker: Child = Command::new(&spt_bin)\n 209|        .no_window()\n 210|        .args([\"daemon\", \"run\"])\n 211|        .env(\"SPT_HOME\", home.path())\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 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...\n 249|    let fresh_online = wait_for_status(fresh, STATUS_ONLINE, Duration::from_secs(20));\n*250|    let fresh_served = rc_sees_tick(&spt_bin, home.path(), fresh, Duration::from_secs(10));\n 251|\n 252|    // ── (7) L3 — the dead victim endpoint is OFFLINED within one reconcile tick\n 253|    //    (LIVE_RECONCILE_INTERVAL_MS = 5 s; allow a generous window). The broker's\n...\n 259|    let stop = {\n*260|        let mut cmd = Command::new(&spt_bin);\n 261|        // `--force`: `wedge2` is a live hosted session by design, so the W3\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...\n 279|    // ── (9) Reap scoped: both harness children + any hosted psyches + brain + broker\n*280|    //    (both rc operators were already reaped inside rc_sees_tick). Never\n 281|    //    machine-wide. ──\n 282|    if let Some(p) = harness_pid2 {\n 283|        kill_pid(p);\n## bind_adapter_profile_persist_e2e.rs\n 126|    let out = {\n*127|        let mut cmd = Command::new(&spt_bin);\n 128|        cmd.no_window()\n 129|            .args([\n 130|                \"api\",\n## bind_cwd_project_e2e.rs\n 150|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*151|    let mut broker: Child = Command::new(&spt_bin)\n 152|        .no_window()\n 153|        .args([\"daemon\", \"run\"])\n 154|        .env(\"SPT_HOME\", home.path())\n...\n 174|    let run = {\n*175|        let mut cmd = Command::new(&spt_bin);\n 176|        cmd.no_window()\n 177|            .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n 178|            .env(\"SPT_HOME\", home.path());\n...\n 229|    let _ = {\n*230|        let mut cmd = Command::new(&spt_bin);\n 231|        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n 232|        output_bounded(cmd, Duration::from_secs(20))\n 233|    };\n## bind_honest_cross_perch_e2e.rs\n 116|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*117|    let mut broker: Child = Command::new(&spt_bin)\n 118|        .no_window()\n 119|        .args([\"daemon\", \"run\"])\n 120|        .env(\"SPT_HOME\", home.path())\n...\n 154|    let attack = {\n*155|        let mut cmd = Command::new(&spt_bin);\n 156|        cmd.no_window()\n 157|            .args([\"api\", \"presence\", victim_id, \"--session-id\", foreign_sid])\n 158|            .env(\"SPT_HOME\", home.path())\n## boundary_ready_strand_e2e.rs\n 95|    let spt = |args: &[&str]| {\n*96|        let mut cmd = Command::new(&spt_bin);\n 97|        cmd.no_window()\n 98|            .args(args)\n 99|            .env(\"SPT_HOME\", home.path())\n## brain_respawn_rename.rs\n 132|            .expect(\"open the new P to append padding\");\n*133|        f.write_all(&[0u8; 4096]).expect(\"append padding -> bytes B\");\n 134|    }\n 135|    let hash_b = file_hash(&canonical);\n 136|    assert_ne!(hash_a, hash_b, \"the swap must flip the binary hash\");\n## brain_split.rs\n 85|fn daemon_running(spt_bin: &PathBuf, home: &std::path::Path) -> bool {\n*86|    let out = Command::new(spt_bin)\n 87|        .no_window()\n 88|        .args([\"daemon\", \"status\"])\n 89|        .env(\"SPT_HOME\", home)\n...\n 110|\n*111|    let mut broker: Child = Command::new(&spt_bin)\n 112|        .no_window()\n 113|        .args([\"daemon\", \"run\"])\n 114|        .env(\"SPT_HOME\", home.path())\n...\n 175|    // Start the REAL broker process (`spt daemon run`) on an isolated home.\n*176|    let mut broker: Child = Command::new(&spt_bin)\n 177|        .no_window()\n 178|        .args([\"daemon\", \"run\"])\n 179|        .env(\"SPT_HOME\", home.path())\n## brain_survive.rs\n 216|            .expect(\"open fixture B to append padding\");\n*217|        f.write_all(&[0u8; 4096]).expect(\"append padding to fixture B\");\n 218|    }\n 219|    let hash_a = file_hash(&fixture_a);\n 220|    let hash_b = file_hash(&fixture_b);\n## composite_e2e.rs\n 177|    let out = {\n*178|        let mut cmd = Command::new(&spt);\n 179|        cmd.no_window()\n 180|            .arg(\"update\")\n 181|            .env(\"SPT_HOME\", &home)\n## contract_e2e.rs\n 146|    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).\n*147|    let poll = Command::new(&spt_bin).no_window()\n 148|        .args([\n 149|            \"api\",\n 150|            \"--adapter\",\n...\n 168|    // exercised through the real binary).\n*169|    let unauth = Command::new(&spt_bin).no_window()\n 170|        .args([\"api\", \"--adapter\", \"mock\", \"poll\", id])\n 171|        .env(\"SPT_HOME\", home.path())\n 172|        .env_remove(\"OWL_SESSION_ID\")\n...\n 202|    // Seed the startup record keyed by the anchor pid.\n*203|    let seed = Command::new(&spt_bin).no_window()\n 204|        .args([\n 205|            \"api\",\n 206|            \"--adapter\",\n...\n 220|    // binds the perch, drains the backlog, and exits (--once).\n*221|    let listen = Command::new(&spt_bin).no_window()\n 222|        .args([\n 223|            \"api\",\n 224|            \"--adapter\",\n...\n 311|    // Seed the startup record keyed by the anchor pid.\n*312|    let seed = Command::new(&spt_bin).no_window()\n 313|        .args([\n 314|            \"api\",\n 315|            \"--adapter\",\n...\n 352|    // THIS process. The brain hosts the lifecycle (driven below).\n*353|    let listen = Command::new(&spt_bin).no_window()\n 354|        .args([\n 355|            \"api\",\n 356|            \"--adapter\",\n...\n 425|    // 3. Graceful shutdown fires the echo-commune BEFORE teardown (3.3).\n*426|    let shutdown = Command::new(&spt_bin).no_window()\n 427|        .args([\n 428|            \"api\",\n 429|            \"--adapter\",\n...\n 492|    // Cold seed — auto-starts the daemon and PUTs the seed into its memory.\n*493|    let seed = Command::new(&spt_bin).no_window()\n 494|        .args([\n 495|            \"api\",\n 496|            \"--adapter\",\n...\n 521|    // Listen TAKEs the seed from the daemon, binds, drains the backlog, exits.\n*522|    let listen = Command::new(&spt_bin).no_window()\n 523|        .args([\n 524|            \"api\",\n 525|            \"--adapter\",\n## daemon_refresh_e2e.rs\n 130|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*131|    let mut broker: Child = Command::new(&spt_bin)\n 132|        .no_window()\n 133|        .args([\"daemon\", \"run\"])\n 134|        .env(\"SPT_HOME\", home.path())\n...\n 157|    let run = {\n*158|        let mut cmd = Command::new(&spt_bin);\n 159|        cmd.no_window()\n 160|            .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])\n 161|            .env(\"SPT_HOME\", home.path());\n...\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...\n 235|    let _ = {\n*236|        let mut cmd = Command::new(&spt_bin);\n 237|        cmd.no_window()\n 238|            .args([\"daemon\", \"stop\", \"--force\"])\n 239|            .env(\"SPT_HOME\", home.path());\n## docs_bundle_e2e.rs\n 127|    }\n*128|    let out = Command::new(spt)\n 129|        .args([\"update\", \"apply\"])\n 130|        .env(\"SPT_HOME\", home)\n 131|        .output()\n## drive_e2e.rs\n 80|    let spt = |args: &[&str]| {\n*81|        let mut cmd = Command::new(&spt_bin);\n 82|        cmd.no_window().args(args).env(\"SPT_HOME\", home.path());\n 83|        output_bounded(cmd, Duration::from_secs(60))\n 84|    };\n## dummy_harness_e2e.rs\n 146|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*147|    let mut broker: Child = Command::new(&spt_bin)\n 148|        .no_window()\n 149|        .args([\"daemon\", \"run\"])\n 150|        .env(\"SPT_HOME\", home.path())\n...\n 171|    let run = {\n*172|        let mut cmd = Command::new(&spt_bin);\n 173|        cmd.no_window()\n 174|            .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n 175|            .env(\"SPT_HOME\", home.path());\n...\n 209|    let rc_err_file = std::fs::File::create(&rc_err).expect(\"create rc stderr log\");\n*210|    let mut rc: Child = Command::new(&spt_bin)\n 211|        .no_window()\n*212|        .args([\"rc\", id])\n 213|        .env(\"SPT_HOME\", home.path())\n 214|        .stdout(Stdio::piped())\n 215|        .stderr(Stdio::from(rc_err_file))\n...\n 218|        .expect(\"spawn spt rc <id>\");\n*219|    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window\n*220|    let mut rc_out = rc.stdout.take().unwrap();\n 221|    let (tx, rx) = std::sync::mpsc::channel::<String>();\n 222|    std::thread::spawn(move || {\n 223|        let mut buf = [0u8; 4096];\n...\n 272|    let _ = {\n*273|        let mut cmd = Command::new(&spt_bin);\n 274|        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n 275|        output_bounded(cmd, Duration::from_secs(20))\n 276|    };\n...\n 402|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*403|    let mut broker: Child = Command::new(&spt_bin)\n 404|        .no_window()\n 405|        .args([\"daemon\", \"run\"])\n 406|        .env(\"SPT_HOME\", home.path())\n...\n 440|    let start_run = {\n*441|        let mut cmd = Command::new(&spt_bin);\n 442|        cmd.no_window()\n 443|            .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n 444|            .env(\"SPT_HOME\", home.path());\n...\n 497|    let run_err_file = std::fs::File::create(&run_err).expect(\"create endpoint-run stderr log\");\n*498|    let mut run: Child = Command::new(&spt_bin)\n 499|        .no_window()\n 500|        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id])\n 501|        .env(\"SPT_HOME\", home.path())\n...\n 506|        .expect(\"spawn endpoint run (no --start, attach-by-default)\");\n*507|    let _run_stdin = run.stdin.take(); // hold the write end open for the attach window\n*508|    let mut run_out = run.stdout.take().unwrap();\n 509|    let (tx, rx) = std::sync::mpsc::channel::<String>();\n 510|    std::thread::spawn(move || {\n 511|        let mut buf = [0u8; 4096];\n...\n 575|    let _ = {\n*576|        let mut cmd = Command::new(&spt_bin);\n 577|        cmd.no_window()\n 578|            .args([\"daemon\", \"stop\"])\n 579|            .env(\"SPT_HOME\", home.path());\n## endpoint_autostart_e2e.rs\n 203|    let log_a = std::fs::File::create(&daemon_a_log).expect(\"create daemon A log\");\n*204|    let mut broker_a: Child = Command::new(&spt_bin)\n 205|        .no_window()\n 206|        .args([\"daemon\", \"run\"])\n 207|        .env(\"SPT_HOME\", home.path())\n...\n 226|    let run = {\n*227|        let mut cmd = Command::new(&spt_bin);\n 228|        cmd.no_window()\n 229|            .args([\n 230|                \"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\", \"--save\",\n...\n 276|    let _ = {\n*277|        let mut cmd = Command::new(&spt_bin);\n 278|        cmd.no_window()\n 279|            .args([\"daemon\", \"stop\", \"--force\"])\n 280|            .env(\"SPT_HOME\", home.path());\n...\n 291|    let log_b = std::fs::File::create(&daemon_b_log).expect(\"create daemon B log\");\n*292|    let mut broker_b: Child = Command::new(&spt_bin)\n 293|        .no_window()\n 294|        .args([\"daemon\", \"run\"])\n 295|        .env(\"SPT_HOME\", home.path())\n...\n 353|    let _ = {\n*354|        let mut cmd = Command::new(&spt_bin);\n 355|        cmd.no_window()\n 356|            .args([\"daemon\", \"stop\", \"--force\"])\n 357|            .env(\"SPT_HOME\", home.path());\n## gateway_e2e.rs\n 84|) -> bool {\n*85|    let mut child = Command::new(spt)\n 86|        .no_window()\n 87|        .args([\"send\", target, \"--from\", from, \"--user-msg\"])\n 88|        .env(\"SPT_HOME\", home)\n...\n 98|        .unwrap()\n*99|        .write_all(body.as_bytes())\n 100|        .unwrap();\n 101|    child.wait_with_output().expect(\"send output\").status.success()\n 102|}\n...\n 190|    let agent_session = \"sid-agent-1\";\n*191|    let bind = Command::new(&spt_bin)\n 192|        .no_window()\n 193|        .args([\n 194|            \"api\",\n## gateway_owner_shell_e2e.rs\n 112|            let mut child = cmd.spawn()?;\n*113|            child.stdin.take().unwrap().write_all(&input)?;\n 114|            child.wait_with_output()\n 115|        })();\n 116|        let _ = tx.send(res);\n...\n 147|    let spt = |args: &[&str]| {\n*148|        let mut cmd = Command::new(&spt_bin);\n 149|        cmd.no_window().args(args).env(\"SPT_HOME\", home.path());\n 150|        output_bounded(cmd, Duration::from_secs(60))\n 151|    };\n 152|    let spt_stdin = |args: &[&str], input: Vec<u8>| {\n*153|        let mut cmd = Command::new(&spt_bin);\n 154|        cmd.no_window().args(args).env(\"SPT_HOME\", home.path());\n 155|        output_with_stdin(cmd, input, Duration::from_secs(60))\n 156|    };\n## idle_edge_drain_e2e.rs\n 140|    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*141|    let mut broker: Child = Command::new(&spt_bin)\n 142|        .no_window()\n 143|        .args([\"daemon\", \"run\"])\n 144|        .env(\"SPT_HOME\", home.path())\n...\n 165|    let run = {\n*166|        let mut cmd = Command::new(&spt_bin);\n 167|        cmd.no_window()\n 168|            .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])\n 169|            .env(\"SPT_HOME\", home.path());\n...\n 219|    let state = {\n*220|        let mut cmd = Command::new(&spt_bin);\n 221|        cmd.no_window()\n 222|            .args([\"api\", \"state\", \"idle\", id, \"--session-id\", &session_id])\n 223|            .env(\"SPT_HOME\", home.path());\n...\n 259|    let _ = {\n*260|        let mut cmd = Command::new(&spt_bin);\n 261|        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n 262|        output_bounded(cmd, Duration::from_secs(20))\n 263|    };\n\nShowing files 1-20 of 54. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":71,"fileCount":20,"files":["crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/tests/adapter_post_step.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/bind_adapter_profile_persist_e2e.rs","crates/spt/tests/bind_cwd_project_e2e.rs","crates/spt/tests/bind_honest_cross_perch_e2e.rs","crates/spt/tests/boundary_ready_strand_e2e.rs","crates/spt/tests/brain_respawn_rename.rs","crates/spt/tests/brain_split.rs","crates/spt/tests/brain_survive.rs","crates/spt/tests/composite_e2e.rs","crates/spt/tests/contract_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt/tests/drive_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/tests/endpoint_autostart_e2e.rs","crates/spt/tests/gateway_e2e.rs","crates/spt/tests/gateway_owner_shell_e2e.rs","crates/spt/tests/idle_edge_drain_e2e.rs"],"fileMatches":[{"path":"crates/spt/tests/active_only_never_relay_e2e.rs","count":2},{"path":"crates/spt/tests/adapter_post_step.rs","count":1},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":12},{"path":"crates/spt/tests/bind_adapter_profile_persist_e2e.rs","count":1},{"path":"crates/spt/tests/bind_cwd_project_e2e.rs","count":3},{"path":"crates/spt/tests/bind_honest_cross_perch_e2e.rs","count":2},{"path":"crates/spt/tests/boundary_ready_strand_e2e.rs","count":1},{"path":"crates/spt/tests/brain_respawn_rename.rs","count":1},{"path":"crates/spt/tests/brain_split.rs","count":3},{"path":"crates/spt/tests/brain_survive.rs","count":1},{"path":"crates/spt/tests/composite_e2e.rs","count":1},{"path":"crates/spt/tests/contract_e2e.rs","count":9},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":4},{"path":"crates/spt/tests/docs_bundle_e2e.rs","count":1},{"path":"crates/spt/tests/drive_e2e.rs","count":1},{"path":"crates/spt/tests/dummy_harness_e2e.rs","count":13},{"path":"crates/spt/tests/endpoint_autostart_e2e.rs","count":5},{"path":"crates/spt/tests/gateway_e2e.rs","count":3},{"path":"crates/spt/tests/gateway_owner_shell_e2e.rs","count":3},{"path":"crates/spt/tests/idle_edge_drain_e2e.rs","count":4}],"truncated":true,"fileLimitReached":20,"displayContent":"# crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 35│fn send_bounded(spt_bin: &std::path::Path, home: &std::path::Path, args: &[&str], body: &str) -> Output {\n*36│    let mut child = Command::new(spt_bin)\n 37│        .no_window()\n 38│        .args(args)\n 39│        .env(\"SPT_HOME\", home)\n   │...\n 48│        .unwrap()\n*49│        .write_all(body.as_bytes())\n 50│        .expect(\"write stdin body\");\n 51│    let (tx, rx) = std::sync::mpsc::channel();\n 52│    std::thread::spawn(move || {\n## adapter_post_step.rs\n 28│fn run_update(spt_bin: &Path, home: &Path, mode: &str, seam_out: &Path) -> std::process::Output {\n*29│    Command::new(spt_bin)\n 30│        .no_window()\n 31│        .args([\"adapter\", \"update\", \"cc\"])\n 32│        .env(\"SPT_HOME\", home)\n## attach_wedge_e2e.rs\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  90│        .env(\"SPT_HOME\", home);\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 125│        .env(\"SPT_HOME\", home)\n 126│        .stdout(Stdio::piped())\n 127│        .stderr(Stdio::null())\n    │...\n 130│        .expect(\"spawn spt rc <id>\");\n*131│    let _rc_stdin = rc.stdin.take();\n*132│    let mut rc_out = rc.stdout.take().unwrap();\n 133│    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 134│    let reader = std::thread::spawn(move || {\n 135│        let mut buf = [0u8; 4096];\n    │...\n 207│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*208│    let mut broker: Child = Command::new(&spt_bin)\n 209│        .no_window()\n 210│        .args([\"daemon\", \"run\"])\n 211│        .env(\"SPT_HOME\", home.path())\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 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    │...\n 249│    let fresh_online = wait_for_status(fresh, STATUS_ONLINE, Duration::from_secs(20));\n*250│    let fresh_served = rc_sees_tick(&spt_bin, home.path(), fresh, Duration::from_secs(10));\n 251│\n 252│    // ── (7) L3 — the dead victim endpoint is OFFLINED within one reconcile tick\n 253│    //    (LIVE_RECONCILE_INTERVAL_MS = 5 s; allow a generous window). The broker's\n    │...\n 259│    let stop = {\n*260│        let mut cmd = Command::new(&spt_bin);\n 261│        // `--force`: `wedge2` is a live hosted session by design, so the W3\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    │...\n 279│    // ── (9) Reap scoped: both harness children + any hosted psyches + brain + broker\n*280│    //    (both rc operators were already reaped inside rc_sees_tick). Never\n 281│    //    machine-wide. ──\n 282│    if let Some(p) = harness_pid2 {\n 283│        kill_pid(p);\n## bind_adapter_profile_persist_e2e.rs\n 126│    let out = {\n*127│        let mut cmd = Command::new(&spt_bin);\n 128│        cmd.no_window()\n 129│            .args([\n 130│                \"api\",\n## bind_cwd_project_e2e.rs\n 150│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*151│    let mut broker: Child = Command::new(&spt_bin)\n 152│        .no_window()\n 153│        .args([\"daemon\", \"run\"])\n 154│        .env(\"SPT_HOME\", home.path())\n    │...\n 174│    let run = {\n*175│        let mut cmd = Command::new(&spt_bin);\n 176│        cmd.no_window()\n 177│            .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n 178│            .env(\"SPT_HOME\", home.path());\n    │...\n 229│    let _ = {\n*230│        let mut cmd = Command::new(&spt_bin);\n 231│        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n 232│        output_bounded(cmd, Duration::from_secs(20))\n 233│    };\n## bind_honest_cross_perch_e2e.rs\n 116│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*117│    let mut broker: Child = Command::new(&spt_bin)\n 118│        .no_window()\n 119│        .args([\"daemon\", \"run\"])\n 120│        .env(\"SPT_HOME\", home.path())\n    │...\n 154│    let attack = {\n*155│        let mut cmd = Command::new(&spt_bin);\n 156│        cmd.no_window()\n 157│            .args([\"api\", \"presence\", victim_id, \"--session-id\", foreign_sid])\n 158│            .env(\"SPT_HOME\", home.path())\n## boundary_ready_strand_e2e.rs\n 95│    let spt = |args: &[&str]| {\n*96│        let mut cmd = Command::new(&spt_bin);\n 97│        cmd.no_window()\n 98│            .args(args)\n 99│            .env(\"SPT_HOME\", home.path())\n## brain_respawn_rename.rs\n 132│            .expect(\"open the new P to append padding\");\n*133│        f.write_all(&[0u8; 4096]).expect(\"append padding -> bytes B\");\n 134│    }\n 135│    let hash_b = file_hash(&canonical);\n 136│    assert_ne!(hash_a, hash_b, \"the swap must flip the binary hash\");\n## brain_split.rs\n  85│fn daemon_running(spt_bin: &PathBuf, home: &std::path::Path) -> bool {\n *86│    let out = Command::new(spt_bin)\n  87│        .no_window()\n  88│        .args([\"daemon\", \"status\"])\n  89│        .env(\"SPT_HOME\", home)\n    │...\n 110│\n*111│    let mut broker: Child = Command::new(&spt_bin)\n 112│        .no_window()\n 113│        .args([\"daemon\", \"run\"])\n 114│        .env(\"SPT_HOME\", home.path())\n    │...\n 175│    // Start the REAL broker process (`spt daemon run`) on an isolated home.\n*176│    let mut broker: Child = Command::new(&spt_bin)\n 177│        .no_window()\n 178│        .args([\"daemon\", \"run\"])\n 179│        .env(\"SPT_HOME\", home.path())\n## brain_survive.rs\n 216│            .expect(\"open fixture B to append padding\");\n*217│        f.write_all(&[0u8; 4096]).expect(\"append padding to fixture B\");\n 218│    }\n 219│    let hash_a = file_hash(&fixture_a);\n 220│    let hash_b = file_hash(&fixture_b);\n## composite_e2e.rs\n 177│    let out = {\n*178│        let mut cmd = Command::new(&spt);\n 179│        cmd.no_window()\n 180│            .arg(\"update\")\n 181│            .env(\"SPT_HOME\", &home)\n## contract_e2e.rs\n 146│    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).\n*147│    let poll = Command::new(&spt_bin).no_window()\n 148│        .args([\n 149│            \"api\",\n 150│            \"--adapter\",\n    │...\n 168│    // exercised through the real binary).\n*169│    let unauth = Command::new(&spt_bin).no_window()\n 170│        .args([\"api\", \"--adapter\", \"mock\", \"poll\", id])\n 171│        .env(\"SPT_HOME\", home.path())\n 172│        .env_remove(\"OWL_SESSION_ID\")\n    │...\n 202│    // Seed the startup record keyed by the anchor pid.\n*203│    let seed = Command::new(&spt_bin).no_window()\n 204│        .args([\n 205│            \"api\",\n 206│            \"--adapter\",\n    │...\n 220│    // binds the perch, drains the backlog, and exits (--once).\n*221│    let listen = Command::new(&spt_bin).no_window()\n 222│        .args([\n 223│            \"api\",\n 224│            \"--adapter\",\n    │...\n 311│    // Seed the startup record keyed by the anchor pid.\n*312│    let seed = Command::new(&spt_bin).no_window()\n 313│        .args([\n 314│            \"api\",\n 315│            \"--adapter\",\n    │...\n 352│    // THIS process. The brain hosts the lifecycle (driven below).\n*353│    let listen = Command::new(&spt_bin).no_window()\n 354│        .args([\n 355│            \"api\",\n 356│            \"--adapter\",\n    │...\n 425│    // 3. Graceful shutdown fires the echo-commune BEFORE teardown (3.3).\n*426│    let shutdown = Command::new(&spt_bin).no_window()\n 427│        .args([\n 428│            \"api\",\n 429│            \"--adapter\",\n    │...\n 492│    // Cold seed — auto-starts the daemon and PUTs the seed into its memory.\n*493│    let seed = Command::new(&spt_bin).no_window()\n 494│        .args([\n 495│            \"api\",\n 496│            \"--adapter\",\n    │...\n 521│    // Listen TAKEs the seed from the daemon, binds, drains the backlog, exits.\n*522│    let listen = Command::new(&spt_bin).no_window()\n 523│        .args([\n 524│            \"api\",\n 525│            \"--adapter\",\n## daemon_refresh_e2e.rs\n 130│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*131│    let mut broker: Child = Command::new(&spt_bin)\n 132│        .no_window()\n 133│        .args([\"daemon\", \"run\"])\n 134│        .env(\"SPT_HOME\", home.path())\n    │...\n 157│    let run = {\n*158│        let mut cmd = Command::new(&spt_bin);\n 159│        cmd.no_window()\n 160│            .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])\n 161│            .env(\"SPT_HOME\", home.path());\n    │...\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    │...\n 235│    let _ = {\n*236│        let mut cmd = Command::new(&spt_bin);\n 237│        cmd.no_window()\n 238│            .args([\"daemon\", \"stop\", \"--force\"])\n 239│            .env(\"SPT_HOME\", home.path());\n## docs_bundle_e2e.rs\n 127│    }\n*128│    let out = Command::new(spt)\n 129│        .args([\"update\", \"apply\"])\n 130│        .env(\"SPT_HOME\", home)\n 131│        .output()\n## drive_e2e.rs\n 80│    let spt = |args: &[&str]| {\n*81│        let mut cmd = Command::new(&spt_bin);\n 82│        cmd.no_window().args(args).env(\"SPT_HOME\", home.path());\n 83│        output_bounded(cmd, Duration::from_secs(60))\n 84│    };\n## dummy_harness_e2e.rs\n 146│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*147│    let mut broker: Child = Command::new(&spt_bin)\n 148│        .no_window()\n 149│        .args([\"daemon\", \"run\"])\n 150│        .env(\"SPT_HOME\", home.path())\n    │...\n 171│    let run = {\n*172│        let mut cmd = Command::new(&spt_bin);\n 173│        cmd.no_window()\n 174│            .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n 175│            .env(\"SPT_HOME\", home.path());\n    │...\n 209│    let rc_err_file = std::fs::File::create(&rc_err).expect(\"create rc stderr log\");\n*210│    let mut rc: Child = Command::new(&spt_bin)\n 211│        .no_window()\n*212│        .args([\"rc\", id])\n 213│        .env(\"SPT_HOME\", home.path())\n 214│        .stdout(Stdio::piped())\n 215│        .stderr(Stdio::from(rc_err_file))\n    │...\n 218│        .expect(\"spawn spt rc <id>\");\n*219│    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window\n*220│    let mut rc_out = rc.stdout.take().unwrap();\n 221│    let (tx, rx) = std::sync::mpsc::channel::<String>();\n 222│    std::thread::spawn(move || {\n 223│        let mut buf = [0u8; 4096];\n    │...\n 272│    let _ = {\n*273│        let mut cmd = Command::new(&spt_bin);\n 274│        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n 275│        output_bounded(cmd, Duration::from_secs(20))\n 276│    };\n    │...\n 402│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*403│    let mut broker: Child = Command::new(&spt_bin)\n 404│        .no_window()\n 405│        .args([\"daemon\", \"run\"])\n 406│        .env(\"SPT_HOME\", home.path())\n    │...\n 440│    let start_run = {\n*441│        let mut cmd = Command::new(&spt_bin);\n 442│        cmd.no_window()\n 443│            .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n 444│            .env(\"SPT_HOME\", home.path());\n    │...\n 497│    let run_err_file = std::fs::File::create(&run_err).expect(\"create endpoint-run stderr log\");\n*498│    let mut run: Child = Command::new(&spt_bin)\n 499│        .no_window()\n 500│        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id])\n 501│        .env(\"SPT_HOME\", home.path())\n    │...\n 506│        .expect(\"spawn endpoint run (no --start, attach-by-default)\");\n*507│    let _run_stdin = run.stdin.take(); // hold the write end open for the attach window\n*508│    let mut run_out = run.stdout.take().unwrap();\n 509│    let (tx, rx) = std::sync::mpsc::channel::<String>();\n 510│    std::thread::spawn(move || {\n 511│        let mut buf = [0u8; 4096];\n    │...\n 575│    let _ = {\n*576│        let mut cmd = Command::new(&spt_bin);\n 577│        cmd.no_window()\n 578│            .args([\"daemon\", \"stop\"])\n 579│            .env(\"SPT_HOME\", home.path());\n## endpoint_autostart_e2e.rs\n 203│    let log_a = std::fs::File::create(&daemon_a_log).expect(\"create daemon A log\");\n*204│    let mut broker_a: Child = Command::new(&spt_bin)\n 205│        .no_window()\n 206│        .args([\"daemon\", \"run\"])\n 207│        .env(\"SPT_HOME\", home.path())\n    │...\n 226│    let run = {\n*227│        let mut cmd = Command::new(&spt_bin);\n 228│        cmd.no_window()\n 229│            .args([\n 230│                \"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\", \"--save\",\n    │...\n 276│    let _ = {\n*277│        let mut cmd = Command::new(&spt_bin);\n 278│        cmd.no_window()\n 279│            .args([\"daemon\", \"stop\", \"--force\"])\n 280│            .env(\"SPT_HOME\", home.path());\n    │...\n 291│    let log_b = std::fs::File::create(&daemon_b_log).expect(\"create daemon B log\");\n*292│    let mut broker_b: Child = Command::new(&spt_bin)\n 293│        .no_window()\n 294│        .args([\"daemon\", \"run\"])\n 295│        .env(\"SPT_HOME\", home.path())\n    │...\n 353│    let _ = {\n*354│        let mut cmd = Command::new(&spt_bin);\n 355│        cmd.no_window()\n 356│            .args([\"daemon\", \"stop\", \"--force\"])\n 357│            .env(\"SPT_HOME\", home.path());\n## gateway_e2e.rs\n  84│) -> bool {\n *85│    let mut child = Command::new(spt)\n  86│        .no_window()\n  87│        .args([\"send\", target, \"--from\", from, \"--user-msg\"])\n  88│        .env(\"SPT_HOME\", home)\n    │...\n  98│        .unwrap()\n *99│        .write_all(body.as_bytes())\n 100│        .unwrap();\n 101│    child.wait_with_output().expect(\"send output\").status.success()\n 102│}\n    │...\n 190│    let agent_session = \"sid-agent-1\";\n*191│    let bind = Command::new(&spt_bin)\n 192│        .no_window()\n 193│        .args([\n 194│            \"api\",\n## gateway_owner_shell_e2e.rs\n 112│            let mut child = cmd.spawn()?;\n*113│            child.stdin.take().unwrap().write_all(&input)?;\n 114│            child.wait_with_output()\n 115│        })();\n 116│        let _ = tx.send(res);\n    │...\n 147│    let spt = |args: &[&str]| {\n*148│        let mut cmd = Command::new(&spt_bin);\n 149│        cmd.no_window().args(args).env(\"SPT_HOME\", home.path());\n 150│        output_bounded(cmd, Duration::from_secs(60))\n 151│    };\n 152│    let spt_stdin = |args: &[&str], input: Vec<u8>| {\n*153│        let mut cmd = Command::new(&spt_bin);\n 154│        cmd.no_window().args(args).env(\"SPT_HOME\", home.path());\n 155│        output_with_stdin(cmd, input, Duration::from_secs(60))\n 156│    };\n## idle_edge_drain_e2e.rs\n 140│    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n*141│    let mut broker: Child = Command::new(&spt_bin)\n 142│        .no_window()\n 143│        .args([\"daemon\", \"run\"])\n 144│        .env(\"SPT_HOME\", home.path())\n    │...\n 165│    let run = {\n*166│        let mut cmd = Command::new(&spt_bin);\n 167│        cmd.no_window()\n 168│            .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])\n 169│            .env(\"SPT_HOME\", home.path());\n    │...\n 219│    let state = {\n*220│        let mut cmd = Command::new(&spt_bin);\n 221│        cmd.no_window()\n 222│            .args([\"api\", \"state\", \"idle\", id, \"--session-id\", &session_id])\n 223│            .env(\"SPT_HOME\", home.path());\n    │...\n 259│    let _ = {\n*260│        let mut cmd = Command::new(&spt_bin);\n 261│        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n 262│        output_bounded(cmd, Duration::from_secs(20))\n 263│    };"},"isError":false,"timestamp":1784195802116}}
{"type":"message","id":"f03ebda3","parentId":"10ac5ad2","timestamp":"2026-07-16T09:56:42.134Z","message":{"role":"toolResult","toolCallId":"call_DAAxfwCjfgSmbN5rr2PMVXSX|fc_0b42584a8ab0545d016a58aada07108199b282d0bd7d84b545","toolName":"grep","content":[{"type":"text","text":"# crates/spt/tests/\n## resume_no_control_steal_e2e.rs\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 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...\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...\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 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).\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 62|//!   its tick counter KEEPS climbing across the swap.\n 63|//!\n 64|//! So the load-bearing assertion is DETERMINISTIC (output-routing displacement), not the\n...\n 66|//! never does. We also assert the trial still PROMOTES (no regression of\n*67|//! REQ-UPDATE-TRIAL-DRAIN-DRIVE) — with no controller wedged, `old_gen_drained()` is true\n 68|//! and the latched-ready candidate promotes.\n 69|//!\n 70|//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\n...\n 97|\n*98|use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n 99|use spt_daemon::codec::{read_frame, write_frame};\n 100|use spt_daemon::endpoint::broker_socket_name;\n 101|use spt_daemon::frame::{Envelope, Role};\n...\n 112|\n*113|/// How many concurrent broker PTY sessions (each with its own local controller) the\n 114|/// respawn re-attaches. ≥2 to be a genuine MULTI-session rig (the A'-rig had 1); 3 mirrors\n 115|/// the field's \"several PTYs, some stolen\" without over-taxing CI.\n*116|const N_SESSIONS: usize = 3;\n 117|\n 118|fn kill_pid(pid: u32) {\n 119|    #[cfg(windows)]\n...\n 128|/// A child that ticks a line every ~150ms FOREVER (until reaped). Each tick is a\n*129|/// `KIND_OUTPUT` frame to the session's controller — the signal whose CONTINUED arrival at\n 130|/// `A_i` proves `A_i` kept control across the respawn (GREEN) and whose CESSATION proves the\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## trial_drain_drive_e2e.rs\n 2|//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\n*3|//! production `run_brain` self-drives the broker's controller-liveness reap so a\n*4|//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\n 5|//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\n 6|//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\n 7|//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\n...\n 14|//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n*15|//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n*16|//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n 17|//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n 18|//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\n 19|//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\n*20|//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\n 21|//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\n 22|//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\n 23|//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\n*24|//! OWN trial with no coordinated broker restart.\n 25|//!\n 26|//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\n 27|//!\n...\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...\n 38|//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n*39|//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\n 40|//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\n 41|//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\n*42|//! flood session and black-holes as its by:None controller — a session the boot-once\n 43|//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\n*44|//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\n 45|//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\n 46|//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\n 47|//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\n...\n 62|//!\n*63|//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\n 64|//!   wedge and the candidate ready, the trial PROMOTES within the window — the\n 65|//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\n 66|//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\n...\n 108|use spt_daemon::brain::Brain;\n*109|use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n 110|use spt_daemon::codec::{read_frame, write_frame};\n 111|use spt_daemon::endpoint::broker_socket_name;\n 112|use spt_daemon::frame::{Envelope, Role};\n...\n 134|/// A child that emits ONE large BURST then goes IDLE (never reads stdin). The burst\n*135|/// (~1 MiB, single fast write) overflows the non-reading controller conn A's socket recv\n 136|/// buffer so A's broker-side writer parks BLOCKED inside `write_frame` — setting\n 137|/// `write_blocked_since` (the stall-evict/reap signal). Then the child is SILENT, so no\n*138|/// further output reaches the drain: the ORTHOGONAL, output-driven `CONTROLLER_WRITE_DEADLINE`\n 139|/// detach (5s, tripped only by a channel that stays FULL as new chunks arrive) never fires,\n 140|/// and the wedge PERSISTS until the `BRAIN_WRITE_DEADLINE` reap (the fix's heartbeat drive)\n 141|/// clears it. This is the FAITHFUL analog of the field's idle-brain black-hole (broker.rs\n...\n 269|/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads\n*270|/// REAL broker truth (`any_local_controller_wedged`) off the live in-proc broker (the\n 271|/// whole point), and whose readiness is LATCHED behind `armed` — withheld from\n 272|/// `run_trial` until the test has established the old-gen wedge, then read from the\n 273|/// REAL `brain.ready` file the subprocess wrote. `record_promoted` / `record_rolled_back`\n...\n 320|    fn old_gen_drained(&self) -> bool {\n*321|        !self.broker.any_local_controller_wedged()\n 322|    }\n 323|}\n 324|\n...\n 333|#[test]\n*334|fn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\n 335|    let home = tempfile::tempdir().expect(\"tempdir\");\n 336|    std::env::set_var(\"SPT_HOME\", home.path());\n 337|    // Shrink the broker-side brain-write deadline so the old-gen wedge matures in\n...\n 373|        thread::spawn(move || {\n*374|            let restart = BrainRestart::new();\n 375|            supervise_brain(\n 376|                &stop_sup,\n*377|                &restart,\n 378|                Duration::from_millis(50),\n 379|                env_sup.as_ref(),\n 380|                Duration::from_secs(60), // generous window — covers a slow-CI boot (eaten before the\n...\n 417|    //    resume, so resume never covered it. dispatch_spawn pre-attaches A as the\n*418|    //    session's LOCAL controller (by = None) — the old-generation-conn identity.\n 419|    //    A then goes silent: the child's ONE burst fills A's recv buffer, A's broker-side\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 6|//! verb. The recovery story: wedged brain-held state no longer needs a full\n 7|//! daemon bounce that kills every PTY.\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 197|\n*198|    // The broker process must be the SAME OS process (never restarted).\n 199|    let broker_still_running = broker.try_wait().map(|st| st.is_none()).unwrap_or(false);\n 200|\n 201|    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\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## brain_survive.rs\n 2|//! swap**: a hosted PTY child + a live QUIC connection survive a brain-**PROCESS**\n*3|//! restart that lands on a **swapped on-disk binary**, with the broker holding\n 4|//! both endpoints untouched throughout. This is the `int` E2E that re-points the\n 5|//! regression-masked REQ-DAEMON-2 / REQ-UPD-3 evidence off the in-process\n 6|//! `brain_swap.rs` shape onto a real process boundary.\n...\n 18|//!   a live QUIC conn). The broker is modeled **in-process** — faithful, not a\n*19|//!   weakening: the broker is the never-restarting anchor *by design*, so an\n 20|//!   in-process [`Broker`] runs the **same Broker code holding the same\n 21|//!   resources** the real broker would across a brain swap. The claim under test\n 22|//!   is the brain process + the swapped binary + broker-held endpoint survival —\n...\n 68|use spt_daemon::brain::Brain;\n*69|use spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};\n 70|use spt_daemon::codec::write_frame;\n 71|use spt_daemon::endpoint::broker_socket_name;\n 72|use spt_daemon::frame::{Envelope, Role};\n...\n 189|#[test]\n*190|fn pty_and_quic_survive_brain_process_restart_onto_swapped_binary() {\n 191|    // The in-process broker + brain children both resolve `broker_socket_name()`\n 192|    // from SPT_HOME, so the supervised child connects to OUR broker. (The\n 193|    // established daemon-test pattern — daemon_e2e.rs et al. set it the same way.)\n...\n 254|    //    pre-D6 respawn loop (no readiness gate); the swap is driven by the\n*255|    //    planned-restart signal, exactly as a real `apply` drives it (D3-3). ──\n 256|    let ready_path = home.path().join(\"brain.ready\");\n 257|    let stop = Arc::new(AtomicBool::new(false));\n*258|    let restart = Arc::new(BrainRestart::new());\n 259|    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260|    let home_path = home.path().to_path_buf();\n 261|\n...\n 263|        let stop = Arc::clone(&stop);\n*264|        let restart = Arc::clone(&restart);\n 265|        let selected = Arc::clone(&selected);\n 266|        thread::spawn(move || {\n 267|            supervise_brain(\n 268|                &stop,\n*269|                &restart,\n 270|                Duration::from_millis(50),\n 271|                &NoTrialEnv,\n 272|                Duration::from_secs(30),\n...\n 300|    );\n*301|    assert_eq!(broker.session_count(), 1, \"session survived the driver drop + resume\");\n 302|    assert_eq!(\n 303|        broker.session_pid(sid),\n 304|        Some(child_pid),\n...\n 308|\n*309|    // ── SWAP: flip the selected binary to B, then trigger a planned restart\n 310|    //    (what `apply` does: swap on disk, signal the brain to cycle). ──\n 311|    *selected.lock().unwrap() = fixture_b.clone();\n*312|    restart.request();\n 313|\n 314|    // ── Generation 1: the supervisor respawned the brain PROCESS onto the\n 315|    //    SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). ──"}],"details":{"scopePath":"crates/spt/tests/resume_no_control_steal_e2e.rs, crates/spt/tests/trial_drain_drive_e2e.rs, crates/spt/tests/daemon_refresh_e2e.rs, crates/spt/tests/brain_survive.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":66,"fileCount":4,"files":["crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/brain_survive.rs"],"fileMatches":[{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":20},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":20},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":15},{"path":"crates/spt/tests/brain_survive.rs","count":11}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt/tests/\n## resume_no_control_steal_e2e.rs\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  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    │...\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    │...\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  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).\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  62│//!   its tick counter KEEPS climbing across the swap.\n  63│//!\n  64│//! So the load-bearing assertion is DETERMINISTIC (output-routing displacement), not the\n    │...\n  66│//! never does. We also assert the trial still PROMOTES (no regression of\n *67│//! REQ-UPDATE-TRIAL-DRAIN-DRIVE) — with no controller wedged, `old_gen_drained()` is true\n  68│//! and the latched-ready candidate promotes.\n  69│//!\n  70│//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\n    │...\n  97│\n *98│use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n  99│use spt_daemon::codec::{read_frame, write_frame};\n 100│use spt_daemon::endpoint::broker_socket_name;\n 101│use spt_daemon::frame::{Envelope, Role};\n    │...\n 112│\n*113│/// How many concurrent broker PTY sessions (each with its own local controller) the\n 114│/// respawn re-attaches. ≥2 to be a genuine MULTI-session rig (the A'-rig had 1); 3 mirrors\n 115│/// the field's \"several PTYs, some stolen\" without over-taxing CI.\n*116│const N_SESSIONS: usize = 3;\n 117│\n 118│fn kill_pid(pid: u32) {\n 119│    #[cfg(windows)]\n    │...\n 128│/// A child that ticks a line every ~150ms FOREVER (until reaped). Each tick is a\n*129│/// `KIND_OUTPUT` frame to the session's controller — the signal whose CONTINUED arrival at\n 130│/// `A_i` proves `A_i` kept control across the respawn (GREEN) and whose CESSATION proves the\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## trial_drain_drive_e2e.rs\n   2│//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\n  *3│//! production `run_brain` self-drives the broker's controller-liveness reap so a\n  *4│//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\n   5│//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\n   6│//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\n   7│//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\n    │...\n  14│//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n *15│//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n *16│//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n  17│//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n  18│//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\n  19│//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\n *20│//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\n  21│//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\n  22│//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\n  23│//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\n *24│//! OWN trial with no coordinated broker restart.\n  25│//!\n  26│//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\n  27│//!\n    │...\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    │...\n  38│//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n *39│//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\n  40│//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\n  41│//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\n *42│//! flood session and black-holes as its by:None controller — a session the boot-once\n  43│//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\n *44│//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\n  45│//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\n  46│//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\n  47│//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\n    │...\n  62│//!\n *63│//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\n  64│//!   wedge and the candidate ready, the trial PROMOTES within the window — the\n  65│//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\n  66│//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\n    │...\n 108│use spt_daemon::brain::Brain;\n*109│use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\n 110│use spt_daemon::codec::{read_frame, write_frame};\n 111│use spt_daemon::endpoint::broker_socket_name;\n 112│use spt_daemon::frame::{Envelope, Role};\n    │...\n 134│/// A child that emits ONE large BURST then goes IDLE (never reads stdin). The burst\n*135│/// (~1 MiB, single fast write) overflows the non-reading controller conn A's socket recv\n 136│/// buffer so A's broker-side writer parks BLOCKED inside `write_frame` — setting\n 137│/// `write_blocked_since` (the stall-evict/reap signal). Then the child is SILENT, so no\n*138│/// further output reaches the drain: the ORTHOGONAL, output-driven `CONTROLLER_WRITE_DEADLINE`\n 139│/// detach (5s, tripped only by a channel that stays FULL as new chunks arrive) never fires,\n 140│/// and the wedge PERSISTS until the `BRAIN_WRITE_DEADLINE` reap (the fix's heartbeat drive)\n 141│/// clears it. This is the FAITHFUL analog of the field's idle-brain black-hole (broker.rs\n    │...\n 269│/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads\n*270│/// REAL broker truth (`any_local_controller_wedged`) off the live in-proc broker (the\n 271│/// whole point), and whose readiness is LATCHED behind `armed` — withheld from\n 272│/// `run_trial` until the test has established the old-gen wedge, then read from the\n 273│/// REAL `brain.ready` file the subprocess wrote. `record_promoted` / `record_rolled_back`\n    │...\n 320│    fn old_gen_drained(&self) -> bool {\n*321│        !self.broker.any_local_controller_wedged()\n 322│    }\n 323│}\n 324│\n    │...\n 333│#[test]\n*334│fn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\n 335│    let home = tempfile::tempdir().expect(\"tempdir\");\n 336│    std::env::set_var(\"SPT_HOME\", home.path());\n 337│    // Shrink the broker-side brain-write deadline so the old-gen wedge matures in\n    │...\n 373│        thread::spawn(move || {\n*374│            let restart = BrainRestart::new();\n 375│            supervise_brain(\n 376│                &stop_sup,\n*377│                &restart,\n 378│                Duration::from_millis(50),\n 379│                env_sup.as_ref(),\n 380│                Duration::from_secs(60), // generous window — covers a slow-CI boot (eaten before the\n    │...\n 417│    //    resume, so resume never covered it. dispatch_spawn pre-attaches A as the\n*418│    //    session's LOCAL controller (by = None) — the old-generation-conn identity.\n 419│    //    A then goes silent: the child's ONE burst fills A's recv buffer, A's broker-side\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   6│//! verb. The recovery story: wedged brain-held state no longer needs a full\n   7│//! daemon bounce that kills every PTY.\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 197│\n*198│    // The broker process must be the SAME OS process (never restarted).\n 199│    let broker_still_running = broker.try_wait().map(|st| st.is_none()).unwrap_or(false);\n 200│\n 201│    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\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## brain_survive.rs\n   2│//! swap**: a hosted PTY child + a live QUIC connection survive a brain-**PROCESS**\n  *3│//! restart that lands on a **swapped on-disk binary**, with the broker holding\n   4│//! both endpoints untouched throughout. This is the `int` E2E that re-points the\n   5│//! regression-masked REQ-DAEMON-2 / REQ-UPD-3 evidence off the in-process\n   6│//! `brain_swap.rs` shape onto a real process boundary.\n    │...\n  18│//!   a live QUIC conn). The broker is modeled **in-process** — faithful, not a\n *19│//!   weakening: the broker is the never-restarting anchor *by design*, so an\n  20│//!   in-process [`Broker`] runs the **same Broker code holding the same\n  21│//!   resources** the real broker would across a brain swap. The claim under test\n  22│//!   is the brain process + the swapped binary + broker-held endpoint survival —\n    │...\n  68│use spt_daemon::brain::Brain;\n *69│use spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};\n  70│use spt_daemon::codec::write_frame;\n  71│use spt_daemon::endpoint::broker_socket_name;\n  72│use spt_daemon::frame::{Envelope, Role};\n    │...\n 189│#[test]\n*190│fn pty_and_quic_survive_brain_process_restart_onto_swapped_binary() {\n 191│    // The in-process broker + brain children both resolve `broker_socket_name()`\n 192│    // from SPT_HOME, so the supervised child connects to OUR broker. (The\n 193│    // established daemon-test pattern — daemon_e2e.rs et al. set it the same way.)\n    │...\n 254│    //    pre-D6 respawn loop (no readiness gate); the swap is driven by the\n*255│    //    planned-restart signal, exactly as a real `apply` drives it (D3-3). ──\n 256│    let ready_path = home.path().join(\"brain.ready\");\n 257│    let stop = Arc::new(AtomicBool::new(false));\n*258│    let restart = Arc::new(BrainRestart::new());\n 259│    let selected = Arc::new(Mutex::new(fixture_a.clone()));\n 260│    let home_path = home.path().to_path_buf();\n 261│\n    │...\n 263│        let stop = Arc::clone(&stop);\n*264│        let restart = Arc::clone(&restart);\n 265│        let selected = Arc::clone(&selected);\n 266│        thread::spawn(move || {\n 267│            supervise_brain(\n 268│                &stop,\n*269│                &restart,\n 270│                Duration::from_millis(50),\n 271│                &NoTrialEnv,\n 272│                Duration::from_secs(30),\n    │...\n 300│    );\n*301│    assert_eq!(broker.session_count(), 1, \"session survived the driver drop + resume\");\n 302│    assert_eq!(\n 303│        broker.session_pid(sid),\n 304│        Some(child_pid),\n    │...\n 308│\n*309│    // ── SWAP: flip the selected binary to B, then trigger a planned restart\n 310│    //    (what `apply` does: swap on disk, signal the brain to cycle). ──\n 311│    *selected.lock().unwrap() = fixture_b.clone();\n*312│    restart.request();\n 313│\n 314│    // ── Generation 1: the supervisor respawned the brain PROCESS onto the\n 315│    //    SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). ──"},"isError":false,"timestamp":1784195802134}}
{"type":"message","id":"7caa89d4","parentId":"f03ebda3","timestamp":"2026-07-16T09:56:59.780Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying real-process broker harness**\n\n**Comparing existing test harnesses for coverage gaps**"},{"type":"toolCall","id":"call_UDnhwcFuieDYLazS263xV2zB|fc_0b42584a8ab0545d016a58aaebbcf081998979fd40bb625c72","name":"read","arguments":{"i":"Listing workspace crates","path":"crates","selector":null}},{"type":"toolCall","id":"call_trPBxiTfYFmTcxHqcADA6R2I|fc_0b42584a8ab0545d016a58aaebbd008199b4c42167026ca351","name":"grep","arguments":{"i":"Finding mock binary declaration","pattern":"name\\s*=\\s*\"mock-session\"|DUMMY_HARNESS_TICK|struct.*Dummy|heartbeat_ms","path":"Cargo.toml;crates","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_1RhQgkpDxe5la8IpuhGiyRxI|fc_0b42584a8ab0545d016a58aaebbd088199aa68f7042e383e36","name":"read","arguments":{"i":"Reading dummy controller harness","path":"crates/spt/tests/dummy_harness_e2e.rs","selector":"1-330"}},{"type":"toolCall","id":"call_cCP8wNPZ4hy6NWHrLx4ttKHN|fc_0b42584a8ab0545d016a58aaebbd0c8199a9bf4b3fbd2c6dc1","name":"grep","arguments":{"i":"Finding refresh test limitations","pattern":"does NOT|not assert|limitation|single|one hosted|alive|functional|traffic|bidirectional","path":"crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_C4PBdNDyCFfEMoo4wCNs3nNl|fc_0b42584a8ab0545d016a58aaebbd148199bc2696f6d521926b","name":"read","arguments":{"i":"Reading test scheduling config","path":".config/nextest.toml","selector":"1-180"}},{"type":"toolCall","id":"call_PjtBkWMTM8zPSfktrz5SV4HD|fc_0b42584a8ab0545d016a58aaebbd1881998a2bfea7e494f084","name":"grep","arguments":{"i":"Finding real update apply PTY tests","pattern":"update.*apply|apply.*update|spt update|apply_staged|request_brain_restart|restart.request","path":"crates/spt/tests;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":23409,"output":638,"cacheRead":166400,"cacheWrite":0,"totalTokens":190447,"reasoningTokens":255,"cost":{"input":0.11704500000000001,"output":0.01914,"cacheRead":0.0832,"cacheWrite":0,"total":0.219385}},"stopReason":"toolUse","timestamp":1784195802187,"responseId":"resp_0b42584a8ab0545d016a58aada9d288199ac21e0512d26c6a5","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aaddd2e48199bdd260e14cc5192d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrrruJTZo8t6RDlUaAdnQxX98rKUWrZJMGAEFZZCtpgHpAx_BhNP3kDiNHSTtU0gZtTSrEd1utaSBdbW69DJ22S-yedgkZ5lpatkkRuytE8mJx-VMEM51cItb1byW37tLbdB2YQ24stCLCqtpIg-jsdancVjqe8H39dG9Tp2uR9C2DOA-sGtOQklvieW0kiL2sSouc4GcBoJo_WWbVtUYHjNKTU9OPMF8NI1P10ogOoLSiu_wIg0jGeEV7rsNqeuzBWxtb2543dRKjDUjGl3JsAI3rzLAqtUeUZYGB7OPrQ9XjOjJGSxCh48v73Hmt85XzLTmEbqMtRNSJRnNM9NgV5wi0OSwjzW4MBgqS-56DZaI4Zp3y20d4MbOtsxdfV0BCVN1UmwXoHvvisRgRKQ0zV7quK6e6f6mdnt7GTgan9AkSQEzVvyCVUEyOA2SQvfKO5vMP8suJS8NRHAGmSX1Wy_PffoFam4U4w7wrRNxowuSgLPv5znlEtSO6tjqAfAH6l1ltKu9q1-iOGv8u0dkoFaAs4vAHNy5IRhINOEONXwxtzG0NUt_r6HqFQhz4X0G38G593eNqnmGK3bA3xQ7aKPZsDxNjO-SRieMms1U9hwgeReIoEgH_1GDNwzdurvb6eEJFdZ7NDT6KwCUPpvtiBQbtH0611e7VbKC4fCftcEMBipBoREiVUUFHK6OTmEzsmUqzumTXhIhoHE8O6tyUphW-2bOFBi1UwHP1WeT66VgC2I5B5WRdhyKz83KPx4IVwFxwYRP5Og9nlvifuXk3Mv4YNIqOZrMnLEBq3NUY-ERRPfbmzK3KfU1q8R9m6Ybe8OD_JFJYoD6SxPIIQ36368Zu0-NRTXItDyfAEAIGLPfcuHC1NeSzwaotABp8F-aSBE7NrkPE318tBpEC1dBpxp5Bm4kKXfn8_fUaDcqWQDrbKy2j20IYsml5JiZNTplyIOl81lVsW6AlzHBTpSD3m0ct0PxL8EcYLhker5nRRtuyN8F76IIqd0NFV7LiAU9FY0-ci1aiIrws6RaR0pUgdzLLiJaQ6QlyFXZcQO_5E1epHZteCIuEIuRkEQVu9fo2AqhhK_3JY615uSDKKef-xlKRNy8sm5N1Y5I69JggjF8Vrph6mpopklfH012iTKqadv9B6ALZ9l7YPw9d1QhByEGgvI-6fVPRa4DK7pzQTIho5VKMCmX9dHWt3-QCllplf4NDQHXkQaIrATYjlG0sMpE3Ber2t3p3X_9tLSis0xoWTcn_ONrR59YF0dGNi5jhWaSxXeFjlZGNzRLVJ51i0vnu48foP6itPcG8IQ0Mq3lxkKAzIMgOzhFLQOYlHPWsRNbkLVh88sTMVRC3ifOI-49XQP8oZef-NWeMqpUEyYQmHxby6o1IzT1vOjO8AjfwbaVtBfg1Csf-taBFL_ixjs3FtgExYvBROrCIEeajRNC14qqGRnaYkIZK-3nvM72iNB9CWiYeLLJbfE_1ZT_sxWJsKn0LjDxst7hkihGpTfK6mrYFnjX9kKTjHSJpkYsIsHPqiR7D1FeSPtQCP5-Tg5BscTQUTzUfxxM8_iE5DTIKDRE3K8h49CGzRX-8R-CaSoMvqIPdDOdDVR3MrUuY9V2yODJl2FfhpEuA6rCVWWXim-9RfgY3WOoWwaL8URE_2u7mZVCKtxT4Sssc1t--vi62dAoW0m3c0gbQFnVke8rT0QngtHjuyfBrPgRzz199NMzaYFw1ER5-v63Xp6uXzfFQGAKxPIhw8lL6-SBECjEeh0rJgXNapym7Gsu54Cb5utA2_WblQhZBO2XS3cYGvD1twp3Qv-yTG0d5hmiP8Fq6b6I5Ly6yI5VKywYRIWd9ckc1TWOQ-pVRBiklHuYVnblfLLvmCnaoyCXrAbevO8JBnGrz5xlJ4UvOyZfjIwH5lR7gLI4wMNXr3Rand5MBwhIcDSt6x6Mi2h43gmgx0aFMtaZNSn5pjUds1mo_07fbnNJ-XO2XuwtmVmJCCYfv-lvpmeMKjEQ85uv5oojBX2i7gT7y9vnriXjKe9o2YKRHzDtr5qobQ1YyTCdoB3W1YPzhSXrfRkZUTeHjIwloxR_X3nGzbZ0pNhT4xYUqTSRQZmZ5tprmkUqes0f9zqiU_a6ZqQzNqctIXXsV0PlXW5Lqpw4JKpkk8NUkoZRZ0MBzphZpnLAvarwyydpXhtOGe1ST3Z9pt8uAjqYCik7L0HMnM8SHVkWI6NPuqULORxK2BM6cceDBFvyYvUoXo8OMFxJUGWDCCxqFRCN7adnHkeqU54_1R-NLQE9B2nUw7HuYa_88k2QIzb46D62IvKzXbuRKk1pk5KhBuU9kZbNLHNyW9aCAEVC1XPZpmjRud8qa3_HnF0iFXJ6rQJkbuDaOxgDWRAZBuQFR1Hpht8IFhTJAbyvqlBL529aBtymA2kuLVr5PDXsdxX2O3v-WW9n33R_Pdb70MddGiBpzq8JwVxeqbSCB7bTgBQNoILhqUfgnHdtp4PzjQhWAzTpy4OBmqLskuelXsR9N03XQW_5MjSBgVwa6GYbZLGPNw_C7tl5rSdhuNfWnr-C-jgivp_NLpKA==","summary":[{"type":"summary_text","text":"**Identifying real-process broker harness**"},{"type":"summary_text","text":"**Comparing existing test harnesses for coverage gaps**"}]},{"id":"fc_0b42584a8ab0545d016a58aaebbcf081998979fd40bb625c72","type":"function_call","status":"completed","arguments":"{\"i\":\"Listing workspace crates\",\"path\":\"crates\",\"selector\":null}","call_id":"call_UDnhwcFuieDYLazS263xV2zB","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaebbd008199b4c42167026ca351","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding mock binary declaration\",\"pattern\":\"name\\\\s*=\\\\s*\\\"mock-session\\\"|DUMMY_HARNESS_TICK|struct.*Dummy|heartbeat_ms\",\"path\":\"Cargo.toml;crates\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_trPBxiTfYFmTcxHqcADA6R2I","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aaebbd088199aa68f7042e383e36","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dummy controller harness\",\"path\":\"crates/spt/tests/dummy_harness_e2e.rs\",\"selector\":\"1-330\"}","call_id":"call_1RhQgkpDxe5la8IpuhGiyRxI","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaebbd0c8199a9bf4b3fbd2c6dc1","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding refresh test limitations\",\"pattern\":\"does NOT|not assert|limitation|single|one hosted|alive|functional|traffic|bidirectional\",\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_cCP8wNPZ4hy6NWHrLx4ttKHN","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aaebbd148199bc2696f6d521926b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading test scheduling config\",\"path\":\".config/nextest.toml\",\"selector\":\"1-180\"}","call_id":"call_C4PBdNDyCFfEMoo4wCNs3nNl","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaebbd1881998a2bfea7e494f084","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding real update apply PTY tests\",\"pattern\":\"update.*apply|apply.*update|spt update|apply_staged|request_brain_restart|restart.request\",\"path\":\"crates/spt/tests;crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_PjtBkWMTM8zPSfktrz5SV4HD","name":"grep"}]},"duration":17592.866299999878,"ttft":3613.174500000663,"contextSnapshot":{"promptTokens":189809,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UDnhwcFuieDYLazS263xV2zB|fc_0b42584a8ab0545d016a58aaebbcf081998979fd40bb625c72","toolName":"read","startedAt":"2026-07-16T09:56:59.781Z","args":{"path":"crates"},"intent":"Listing workspace crates"},"id":"c631ad94","parentId":"7caa89d4","timestamp":"2026-07-16T09:56:59.781Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_trPBxiTfYFmTcxHqcADA6R2I|fc_0b42584a8ab0545d016a58aaebbd008199b4c42167026ca351","toolName":"grep","startedAt":"2026-07-16T09:56:59.781Z","args":{"path":"Cargo.toml;crates"},"intent":"Finding mock binary declaration"},"id":"b7c00f1b","parentId":"c631ad94","timestamp":"2026-07-16T09:56:59.781Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1RhQgkpDxe5la8IpuhGiyRxI|fc_0b42584a8ab0545d016a58aaebbd088199aa68f7042e383e36","toolName":"read","startedAt":"2026-07-16T09:56:59.781Z","args":{"path":"crates/spt/tests/dummy_harness_e2e.rs"},"intent":"Reading dummy controller harness"},"id":"3e00300f","parentId":"b7c00f1b","timestamp":"2026-07-16T09:56:59.781Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cCP8wNPZ4hy6NWHrLx4ttKHN|fc_0b42584a8ab0545d016a58aaebbd0c8199a9bf4b3fbd2c6dc1","toolName":"grep","startedAt":"2026-07-16T09:56:59.781Z","args":{"path":"crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs"},"intent":"Finding refresh test limitations"},"id":"62e6519b","parentId":"3e00300f","timestamp":"2026-07-16T09:56:59.781Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_C4PBdNDyCFfEMoo4wCNs3nNl|fc_0b42584a8ab0545d016a58aaebbd148199bc2696f6d521926b","toolName":"read","startedAt":"2026-07-16T09:56:59.781Z","args":{"path":".config/nextest.toml"},"intent":"Reading test scheduling config"},"id":"9d19c1dd","parentId":"62e6519b","timestamp":"2026-07-16T09:56:59.781Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PjtBkWMTM8zPSfktrz5SV4HD|fc_0b42584a8ab0545d016a58aaebbd1881998a2bfea7e494f084","toolName":"grep","startedAt":"2026-07-16T09:56:59.781Z","args":{"path":"crates/spt/tests;crates/spt-daemon/tests"},"intent":"Finding real update apply PTY tests"},"id":"f5101565","parentId":"9d19c1dd","timestamp":"2026-07-16T09:56:59.781Z"}
{"type":"message","id":"5dc06d30","parentId":"f5101565","timestamp":"2026-07-16T09:56:59.783Z","message":{"role":"toolResult","toolCallId":"call_UDnhwcFuieDYLazS263xV2zB|fc_0b42584a8ab0545d016a58aaebbcf081998979fd40bb625c72","toolName":"read","content":[{"type":"text","text":".\n  - spt/                              8h ago\n    - tests/                          1h ago\n    - src/                            2h ago\n    - Cargo.toml            7.8KB     8h ago\n  - spt-daemon/                       22h ago\n    - src/                            2h ago\n    - tests/                          22h ago\n    - Cargo.toml            4.2KB     22h ago\n  - spt-runtime/                      23h ago\n    - src/                            23h ago\n    - manifest.schema.json  40.4KB    23h ago\n    - Cargo.toml            1001B     1w ago\n  - spt-store/                        5d ago\n    - src/                            2h ago\n    - tests/                          5d ago\n    - Cargo.toml            823B      5d ago\n  - spt-net/                          1w ago\n    - Cargo.toml            4.0KB     1w ago\n    - src/                            1w ago\n  - xtask/                            1w ago\n    - src/                            22h ago\n    - Cargo.toml            749B      1w ago\n  - spt-term/                         1w ago\n    - src/                            1w ago\n    - tests/                          1w ago\n    - Cargo.toml            1.4KB     1w ago\n  - spt-proto/                        1w ago\n    - src/                            1w ago\n    - Cargo.toml            351B      1w ago\n  - spt-msg/                          1w ago\n    - src/                            7h ago\n    - tests/                          1w ago\n    - Cargo.toml            588B      1w ago\n  - spt-live/                         1w ago\n    - src/                            5d ago\n    - Cargo.toml            636B      1w ago"}],"details":{"isDirectory":true,"resolvedPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates"}}},"isError":false,"timestamp":1784195819783}}
{"type":"message","id":"d87e5a3d","parentId":"5dc06d30","timestamp":"2026-07-16T09:56:59.792Z","message":{"role":"toolResult","toolCallId":"call_1RhQgkpDxe5la8IpuhGiyRxI|fc_0b42584a8ab0545d016a58aaebbd088199aa68f7042e383e36","toolName":"read","content":[{"type":"text","text":"//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent\n//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the\n//! REAL spt-hosted bringup path end-to-end —\n//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach\n//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and\n//! NOT a mock that exits immediately.\n//!\n//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the\n//! harness contract), prints a stdout heartbeat on an interval, and stays alive\n//! until killed — so \"broker wedged / endpoint died\" is cleanly separable from \"a\n//! real harness failed to launch.\" Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,\n//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.\n//!\n//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the\n//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the\n//! live heartbeat output. Bounded; all spawned pids reaped scoped (never\n//! machine-wide — shared runner).\n\nuse std::io::Read;\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command, Output, Stdio};\nuse std::sync::Mutex;\nuse std::time::{Duration, Instant};\n\nmod common;\nuse common::CommandNoWindowExt;\n\nuse spt_store::perch::{self, ParentHint};\n\n// Both tests in this file set the PROCESS-GLOBAL env var SPT_HOME then do in-proc\n// reads of it (perch::spt_home / adapters_dir, registry::register, psychebin\n// fs::copy staging). One file = one test binary → the two tests otherwise run on\n// parallel threads and their set_var/reads race (register lands in the wrong home\n// → ENDPOINT_RUN_ADAPTER_UNREGISTERED; psychebin staging → os-32 sharing\n// collision). Serialize the whole body of each on one file-local lock — the exact\n// isolated-home pattern of contract_e2e.rs:26 / gateway_e2e.rs:35. Child procs\n// already inherit the right home via explicit `.env(\"SPT_HOME\", …)`; only the\n// in-proc reads leak, so serializing the two bodies is sufficient.\nstatic E2E_LOCK: Mutex<()> = Mutex::new(());\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#[test]\nfn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {\n    // Serialize against the sister test — shared process-global SPT_HOME.\n    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\n    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node\n    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces\n    //    as net-stream events, so it requires a net-bound broker — a net-less broker\n    //    refuses the loopback dial (\"net disabled\"). Unlike the livehost/reconcile\n    //    tests (which only inspect perch state), the rc-attach path needs net up. ──\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: a kind=harness adapter whose\n    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};\n    //    the program token is the absolute mock-session path. The dummy shells the\n    //    same `spt` to bind (via --spt). ──\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    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only\n    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —\n    // the online signal is gated to the host predicate), so the declaration is what\n    // this test needs for the online path. A psyche binary fixture (a copy of `spt`)\n    // is staged too; under F-030 the Psyche is a bounded PER-EVENT TURN, not a\n    // resident process the reconcile hosts — no resident `{id}-psyche` is expected,\n    // so the psyche-perch reap at teardown (step 6) is belt-and-suspenders.\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) The REAL spt-hosted bringup: endpoint run --start (broker spawns the\n    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──\n    let id = \"dummy7\";\n    let run = {\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.path());\n        output_bounded(cmd, Duration::from_secs(45))\n    };\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\n    // The dummy binds its perch live within a few seconds of the broker PTY spawn.\n    let mut online = false;\n    let deadline = Instant::now() + Duration::from_secs(20);\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    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`\n    // machine token) — the long-lived PTY child itself, NOT the transient `spt api\n    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).\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    let alive_after = {\n        std::thread::sleep(Duration::from_millis(700));\n        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)\n    };\n\n    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──\n    let rc_err = home.path().join(\"rc.stderr.log\");\n    let rc_err_file = std::fs::File::create(&rc_err).expect(\"create rc stderr log\");\n    let mut rc: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"rc\", id])\n        .env(\"SPT_HOME\", home.path())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::from(rc_err_file))\n        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming\n        .spawn()\n        .expect(\"spawn spt rc <id>\");\n    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window\n    let mut rc_out = rc.stdout.take().unwrap();\n    let (tx, rx) = std::sync::mpsc::channel::<String>();\n    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(acc.clone());\n                        break;\n                    }\n                }\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send(acc);\n    });\n    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();\n    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\n\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();\n    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did\n    // NOT RC_FAIL (no \"no live session\" / loopback-dial refusal).\n    let rc_connected = rc_stderr.contains(\"PUMP_IPC_READER\") && !rc_stderr.contains(\"RC_FAIL\");\n    eprintln!(\n        \"=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \\\n         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \\\n         rc_stdout_bytes={} ===\\n=== endpoint run stderr ===\\n{}\\n=== rc stderr ===\\n{rc_stderr}\\\n         \\n=== rc stdout (first 400) ===\\n{}\",\n        run.status.success(),\n        rc_captured.len(),\n        String::from_utf8_lossy(&run.stderr),\n        rc_captured.chars().take(400).collect::<String>(),\n    );\n\n    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──\n    kill_pid(rc.id());\n    let _ = rc.kill();\n    let _ = rc.wait();\n    if let Some(p) = harness_pid {\n        kill_pid(p);\n    }\n    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).\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    let _ = {\n        let mut cmd = Command::new(&spt_bin);\n        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\n        output_bounded(cmd, Duration::from_secs(20))\n    };\n    kill_pid(brain_pid);\n    let _ = broker.kill();\n    let _ = broker.wait();\n\n    assert!(\n        run.status.success(),\n        \"endpoint run --start must succeed: {}\",\n        String::from_utf8_lossy(&run.stderr)\n    );\n    assert!(\n        online,\n        \"the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\\n\\\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n    assert!(\n        alive_after,\n        \"the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \\\n         default mock — harness_pid={harness_pid:?}\"\n    );\n    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for\n    // \"the attach wired up.\" It is now SUBSUMED by the `rc_saw_tick` keystone gate\n    // below: receiving the harness's live output is a strictly stronger proof of a\n    // wired attach than the setup log line (and the log line races the now-fast\n    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept\n    // informational in the DIAGNOSTIC above; the hard proof is output delivery.\n    let _ = rc_connected;\n    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,\n    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its\n    // `[session.self]` stdout — the operator's \"attach shows no output\" is fixed. This\n    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only\n    // ever attach to an idle/just-spawned child, so `become_controller` replays an\n    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix\n    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the\n    // replay on) is what makes this pass.\n    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n    assert!(\n        rc_saw_tick,\n        \"`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \\\n         output (got {} bytes) — the broker must forward an already-producing harness's \\\n         PTY to the attach without the replay-vs-forward deadlock.\\n=== rc stderr ===\\n{rc_stderr}\\\n         \\n=== brain stderr ===\\n{brain_stderr}\",\n        rc_captured.len()\n    );\n}\n\n// ── REPRO-FIRST regression for REQ-HAZARD-RC-ATTACH-ONLINE-RACE ──────────────\n// THE BUG (doyle root-caused): `spt endpoint run` in an ATTACH terminal action\n// (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In\n// `cmd_endpoint_run` the flow is `launch_harness_brokered_in` (returns once the\n// harness PROCESS is spawned) → `run_attach`, with NO await-online between them.\n// The broker-PTY bind (info status→STATUS_ONLINE + a live session) lands ASYNC,\n// so the immediate `run_attach` loses the race: it either short-circuits on the\n// offline status-gate (\"offline — nothing to attach\", REQ-HAZARD-RC-ATTACH-FAILFAST\n// in rc.rs run_attach) or hits \"no live session for endpoint\" (printed as\n// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.\n//\n// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\n\n[Showing lines 1-333 of 631. Use :334 to continue]"}],"details":{"truncation":{"content":"//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent\r\n//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the\r\n//! REAL spt-hosted bringup path end-to-end —\r\n//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach\r\n//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and\r\n//! NOT a mock that exits immediately.\r\n//!\r\n//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the\r\n//! harness contract), prints a stdout heartbeat on an interval, and stays alive\r\n//! until killed — so \"broker wedged / endpoint died\" is cleanly separable from \"a\r\n//! real harness failed to launch.\" Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,\r\n//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.\r\n//!\r\n//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the\r\n//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the\r\n//! live heartbeat output. Bounded; all spawned pids reaped scoped (never\r\n//! machine-wide — shared 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::sync::Mutex;\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// Both tests in this file set the PROCESS-GLOBAL env var SPT_HOME then do in-proc\r\n// reads of it (perch::spt_home / adapters_dir, registry::register, psychebin\r\n// fs::copy staging). One file = one test binary → the two tests otherwise run on\r\n// parallel threads and their set_var/reads race (register lands in the wrong home\r\n// → ENDPOINT_RUN_ADAPTER_UNREGISTERED; psychebin staging → os-32 sharing\r\n// collision). Serialize the whole body of each on one file-local lock — the exact\r\n// isolated-home pattern of contract_e2e.rs:26 / gateway_e2e.rs:35. Child procs\r\n// already inherit the right home via explicit `.env(\"SPT_HOME\", …)`; only the\r\n// in-proc reads leak, so serializing the two bodies is sufficient.\r\nstatic E2E_LOCK: Mutex<()> = Mutex::new(());\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#[test]\r\nfn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {\r\n    // Serialize against the sister test — shared process-global SPT_HOME.\r\n    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\r\n    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node\r\n    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces\r\n    //    as net-stream events, so it requires a net-bound broker — a net-less broker\r\n    //    refuses the loopback dial (\"net disabled\"). Unlike the livehost/reconcile\r\n    //    tests (which only inspect perch state), the rc-attach path needs net up. ──\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: a kind=harness adapter whose\r\n    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};\r\n    //    the program token is the absolute mock-session path. The dummy shells the\r\n    //    same `spt` to bind (via --spt). ──\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    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only\r\n    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —\r\n    // the online signal is gated to the host predicate), so the declaration is what\r\n    // this test needs for the online path. A psyche binary fixture (a copy of `spt`)\r\n    // is staged too; under F-030 the Psyche is a bounded PER-EVENT TURN, not a\r\n    // resident process the reconcile hosts — no resident `{id}-psyche` is expected,\r\n    // so the psyche-perch reap at teardown (step 6) is belt-and-suspenders.\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) The REAL spt-hosted bringup: endpoint run --start (broker spawns the\r\n    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──\r\n    let id = \"dummy7\";\r\n    let run = {\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.path());\r\n        output_bounded(cmd, Duration::from_secs(45))\r\n    };\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\r\n    // The dummy binds its perch live within a few seconds of the broker PTY spawn.\r\n    let mut online = false;\r\n    let deadline = Instant::now() + Duration::from_secs(20);\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    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`\r\n    // machine token) — the long-lived PTY child itself, NOT the transient `spt api\r\n    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).\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    let alive_after = {\r\n        std::thread::sleep(Duration::from_millis(700));\r\n        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)\r\n    };\r\n\r\n    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──\r\n    let rc_err = home.path().join(\"rc.stderr.log\");\r\n    let rc_err_file = std::fs::File::create(&rc_err).expect(\"create rc stderr log\");\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.path())\r\n        .stdout(Stdio::piped())\r\n        .stderr(Stdio::from(rc_err_file))\r\n        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming\r\n        .spawn()\r\n        .expect(\"spawn spt rc <id>\");\r\n    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window\r\n    let mut rc_out = rc.stdout.take().unwrap();\r\n    let (tx, rx) = std::sync::mpsc::channel::<String>();\r\n    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(acc.clone());\r\n                        break;\r\n                    }\r\n                }\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        let _ = tx.send(acc);\r\n    });\r\n    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();\r\n    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\r\n\r\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\r\n    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();\r\n    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did\r\n    // NOT RC_FAIL (no \"no live session\" / loopback-dial refusal).\r\n    let rc_connected = rc_stderr.contains(\"PUMP_IPC_READER\") && !rc_stderr.contains(\"RC_FAIL\");\r\n    eprintln!(\r\n        \"=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \\\r\n         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \\\r\n         rc_stdout_bytes={} ===\\n=== endpoint run stderr ===\\n{}\\n=== rc stderr ===\\n{rc_stderr}\\\r\n         \\n=== rc stdout (first 400) ===\\n{}\",\r\n        run.status.success(),\r\n        rc_captured.len(),\r\n        String::from_utf8_lossy(&run.stderr),\r\n        rc_captured.chars().take(400).collect::<String>(),\r\n    );\r\n\r\n    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──\r\n    kill_pid(rc.id());\r\n    let _ = rc.kill();\r\n    let _ = rc.wait();\r\n    if let Some(p) = harness_pid {\r\n        kill_pid(p);\r\n    }\r\n    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).\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    let _ = {\r\n        let mut cmd = Command::new(&spt_bin);\r\n        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\r\n        output_bounded(cmd, Duration::from_secs(20))\r\n    };\r\n    kill_pid(brain_pid);\r\n    let _ = broker.kill();\r\n    let _ = broker.wait();\r\n\r\n    assert!(\r\n        run.status.success(),\r\n        \"endpoint run --start must succeed: {}\",\r\n        String::from_utf8_lossy(&run.stderr)\r\n    );\r\n    assert!(\r\n        online,\r\n        \"the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        alive_after,\r\n        \"the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \\\r\n         default mock — harness_pid={harness_pid:?}\"\r\n    );\r\n    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for\r\n    // \"the attach wired up.\" It is now SUBSUMED by the `rc_saw_tick` keystone gate\r\n    // below: receiving the harness's live output is a strictly stronger proof of a\r\n    // wired attach than the setup log line (and the log line races the now-fast\r\n    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept\r\n    // informational in the DIAGNOSTIC above; the hard proof is output delivery.\r\n    let _ = rc_connected;\r\n    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,\r\n    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its\r\n    // `[session.self]` stdout — the operator's \"attach shows no output\" is fixed. This\r\n    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only\r\n    // ever attach to an idle/just-spawned child, so `become_controller` replays an\r\n    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix\r\n    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the\r\n    // replay on) is what makes this pass.\r\n    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n    assert!(\r\n        rc_saw_tick,\r\n        \"`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \\\r\n         output (got {} bytes) — the broker must forward an already-producing harness's \\\r\n         PTY to the attach without the replay-vs-forward deadlock.\\n=== rc stderr ===\\n{rc_stderr}\\\r\n         \\n=== brain stderr ===\\n{brain_stderr}\",\r\n        rc_captured.len()\r\n    );\r\n}\r\n\r\n// ── REPRO-FIRST regression for REQ-HAZARD-RC-ATTACH-ONLINE-RACE ──────────────\r\n// THE BUG (doyle root-caused): `spt endpoint run` in an ATTACH terminal action\r\n// (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In\r\n// `cmd_endpoint_run` the flow is `launch_harness_brokered_in` (returns once the\r\n// harness PROCESS is spawned) → `run_attach`, with NO await-online between them.\r\n// The broker-PTY bind (info status→STATUS_ONLINE + a live session) lands ASYNC,\r\n// so the immediate `run_attach` loses the race: it either short-circuits on the\r\n// offline status-gate (\"offline — nothing to attach\", REQ-HAZARD-RC-ATTACH-FAILFAST\r\n// in rc.rs run_attach) or hits \"no live session for endpoint\" (printed as\r\n// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.\r\n//\r\n// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\r","truncated":true,"truncatedBy":"lines","totalLines":631,"totalBytes":15993,"outputLines":333,"outputBytes":15993,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent\r\n//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the\r\n//! REAL spt-hosted bringup path end-to-end —\r\n//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach\r\n//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and\r\n//! NOT a mock that exits immediately.\r\n//!\r\n//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the\r\n//! harness contract), prints a stdout heartbeat on an interval, and stays alive\r\n//! until killed — so \"broker wedged / endpoint died\" is cleanly separable from \"a\r\n//! real harness failed to launch.\" Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,\r\n//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.\r\n//!\r\n//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the\r\n//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the\r\n//! live heartbeat output. Bounded; all spawned pids reaped scoped (never\r\n//! machine-wide — shared 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::sync::Mutex;\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// Both tests in this file set the PROCESS-GLOBAL env var SPT_HOME then do in-proc\r\n// reads of it (perch::spt_home / adapters_dir, registry::register, psychebin\r\n// fs::copy staging). One file = one test binary → the two tests otherwise run on\r\n// parallel threads and their set_var/reads race (register lands in the wrong home\r\n// → ENDPOINT_RUN_ADAPTER_UNREGISTERED; psychebin staging → os-32 sharing\r\n// collision). Serialize the whole body of each on one file-local lock — the exact\r\n// isolated-home pattern of contract_e2e.rs:26 / gateway_e2e.rs:35. Child procs\r\n// already inherit the right home via explicit `.env(\"SPT_HOME\", …)`; only the\r\n// in-proc reads leak, so serializing the two bodies is sufficient.\r\nstatic E2E_LOCK: Mutex<()> = Mutex::new(());\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#[test]\r\nfn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {\r\n    // Serialize against the sister test — shared process-global SPT_HOME.\r\n    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\r\n    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node\r\n    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces\r\n    //    as net-stream events, so it requires a net-bound broker — a net-less broker\r\n    //    refuses the loopback dial (\"net disabled\"). Unlike the livehost/reconcile\r\n    //    tests (which only inspect perch state), the rc-attach path needs net up. ──\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: a kind=harness adapter whose\r\n    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};\r\n    //    the program token is the absolute mock-session path. The dummy shells the\r\n    //    same `spt` to bind (via --spt). ──\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    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only\r\n    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —\r\n    // the online signal is gated to the host predicate), so the declaration is what\r\n    // this test needs for the online path. A psyche binary fixture (a copy of `spt`)\r\n    // is staged too; under F-030 the Psyche is a bounded PER-EVENT TURN, not a\r\n    // resident process the reconcile hosts — no resident `{id}-psyche` is expected,\r\n    // so the psyche-perch reap at teardown (step 6) is belt-and-suspenders.\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) The REAL spt-hosted bringup: endpoint run --start (broker spawns the\r\n    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──\r\n    let id = \"dummy7\";\r\n    let run = {\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.path());\r\n        output_bounded(cmd, Duration::from_secs(45))\r\n    };\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\r\n    // The dummy binds its perch live within a few seconds of the broker PTY spawn.\r\n    let mut online = false;\r\n    let deadline = Instant::now() + Duration::from_secs(20);\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    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`\r\n    // machine token) — the long-lived PTY child itself, NOT the transient `spt api\r\n    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).\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    let alive_after = {\r\n        std::thread::sleep(Duration::from_millis(700));\r\n        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)\r\n    };\r\n\r\n    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──\r\n    let rc_err = home.path().join(\"rc.stderr.log\");\r\n    let rc_err_file = std::fs::File::create(&rc_err).expect(\"create rc stderr log\");\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.path())\r\n        .stdout(Stdio::piped())\r\n        .stderr(Stdio::from(rc_err_file))\r\n        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming\r\n        .spawn()\r\n        .expect(\"spawn spt rc <id>\");\r\n    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window\r\n    let mut rc_out = rc.stdout.take().unwrap();\r\n    let (tx, rx) = std::sync::mpsc::channel::<String>();\r\n    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(acc.clone());\r\n                        break;\r\n                    }\r\n                }\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        let _ = tx.send(acc);\r\n    });\r\n    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();\r\n    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\r\n\r\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\r\n    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();\r\n    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did\r\n    // NOT RC_FAIL (no \"no live session\" / loopback-dial refusal).\r\n    let rc_connected = rc_stderr.contains(\"PUMP_IPC_READER\") && !rc_stderr.contains(\"RC_FAIL\");\r\n    eprintln!(\r\n        \"=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \\\r\n         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \\\r\n         rc_stdout_bytes={} ===\\n=== endpoint run stderr ===\\n{}\\n=== rc stderr ===\\n{rc_stderr}\\\r\n         \\n=== rc stdout (first 400) ===\\n{}\",\r\n        run.status.success(),\r\n        rc_captured.len(),\r\n        String::from_utf8_lossy(&run.stderr),\r\n        rc_captured.chars().take(400).collect::<String>(),\r\n    );\r\n\r\n    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──\r\n    kill_pid(rc.id());\r\n    let _ = rc.kill();\r\n    let _ = rc.wait();\r\n    if let Some(p) = harness_pid {\r\n        kill_pid(p);\r\n    }\r\n    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).\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    let _ = {\r\n        let mut cmd = Command::new(&spt_bin);\r\n        cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());\r\n        output_bounded(cmd, Duration::from_secs(20))\r\n    };\r\n    kill_pid(brain_pid);\r\n    let _ = broker.kill();\r\n    let _ = broker.wait();\r\n\r\n    assert!(\r\n        run.status.success(),\r\n        \"endpoint run --start must succeed: {}\",\r\n        String::from_utf8_lossy(&run.stderr)\r\n    );\r\n    assert!(\r\n        online,\r\n        \"the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        alive_after,\r\n        \"the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \\\r\n         default mock — harness_pid={harness_pid:?}\"\r\n    );\r\n    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for\r\n    // \"the attach wired up.\" It is now SUBSUMED by the `rc_saw_tick` keystone gate\r\n    // below: receiving the harness's live output is a strictly stronger proof of a\r\n    // wired attach than the setup log line (and the log line races the now-fast\r\n    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept\r\n    // informational in the DIAGNOSTIC above; the hard proof is output delivery.\r\n    let _ = rc_connected;\r\n    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,\r\n    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its\r\n    // `[session.self]` stdout — the operator's \"attach shows no output\" is fixed. This\r\n    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only\r\n    // ever attach to an idle/just-spawned child, so `become_controller` replays an\r\n    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix\r\n    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the\r\n    // replay on) is what makes this pass.\r\n    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n    assert!(\r\n        rc_saw_tick,\r\n        \"`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \\\r\n         output (got {} bytes) — the broker must forward an already-producing harness's \\\r\n         PTY to the attach without the replay-vs-forward deadlock.\\n=== rc stderr ===\\n{rc_stderr}\\\r\n         \\n=== brain stderr ===\\n{brain_stderr}\",\r\n        rc_captured.len()\r\n    );\r\n}\r\n\r\n// ── REPRO-FIRST regression for REQ-HAZARD-RC-ATTACH-ONLINE-RACE ──────────────\r\n// THE BUG (doyle root-caused): `spt endpoint run` in an ATTACH terminal action\r\n// (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In\r\n// `cmd_endpoint_run` the flow is `launch_harness_brokered_in` (returns once the\r\n// harness PROCESS is spawned) → `run_attach`, with NO await-online between them.\r\n// The broker-PTY bind (info status→STATUS_ONLINE + a live session) lands ASYNC,\r\n// so the immediate `run_attach` loses the race: it either short-circuits on the\r\n// offline status-gate (\"offline — nothing to attach\", REQ-HAZARD-RC-ATTACH-FAILFAST\r\n// in rc.rs run_attach) or hits \"no live session for endpoint\" (printed as\r\n// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.\r\n//\r\n// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,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\\tests\\dummy_harness_e2e.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":631,"totalBytes":15993,"outputLines":333,"outputBytes":15993,"shownRange":{"start":1,"end":333},"nextOffset":334}}},"isError":false,"timestamp":1784195819792}}
{"type":"message","id":"687b3df4","parentId":"d87e5a3d","timestamp":"2026-07-16T09:56:59.795Z","message":{"role":"toolResult","toolCallId":"call_C4PBdNDyCFfEMoo4wCNs3nNl|fc_0b42584a8ab0545d016a58aaebbd148199bc2696f6d521926b","toolName":"read","content":[{"type":"text","text":"# cargo-nextest configuration.\n#\n# CI runs the workspace test suite under `cargo nextest run` in TWO PHASES (see the\n# `test` job in .github/workflows/ci.yml):\n#   Phase A  `-E 'not ( <HEAVY> )'`  — the light pool, full parallelism, fast.\n#   Phase B  `-E '<HEAVY>'`          — the heavy real-broker/PTY/daemon-tree class,\n#                                      run AFTER Phase A so the box is QUIET, and\n#                                      serialized by the test-group below.\n# <HEAVY> is the SAME filter string in both CI phases AND in the test-group override\n# at the bottom of this file — keep all three in sync (a clean partition: A is the\n# exact negation of B, so nothing is dropped or double-run).\n# See docs/RELEASE-RUNBOOK + ci-kitsubito.\n\n[profile.default]\n# A hung test (e.g. a blocking PTY/socket read that never completes) must FAIL FAST,\n# never wedge the CI job up to its wall-clock timeout. Warn once a test exceeds 60s,\n# then SIGKILL + fail it after 4 such periods (4 minutes). No legitimate test in this\n# workspace runs that long; this is purely the no-infinite-wedge backstop.\nslow-timeout = { period = \"60s\", terminate-after = 4 }\n\n# Concurrency cap for the heavy real-broker / PTY / daemon-tree integration tests.\n#\n# WHY: nextest fans a phase into ONE parallel pool sized to the runner's core count.\n# The \"heavy\" tests each spin a REAL broker, forkpty a REAL PTY child, and/or spawn a\n# REAL `spt daemon`/brain process tree. Two failure modes were observed on the shared\n# self-hosted runners:\n#   1. heavy-vs-heavy contention: ~16 real brokers launching at once contend for the\n#      scheduler / PTY allocation / socket setup, producing wait-deadline / PTY-spawn\n#      / child-reap TIMING flakes whose victim rotates run to run (spawn_env on Linux\n#      forkpty, g4 on Windows, controller_writer, attach_survives, …).\n#   2. heavy-starved-by-light: even with the heavy class serialized, the ~1200 LIGHT\n#      tests still ran at full parallelism in the SAME pool and saturated the box, so\n#      a heavy test's helper thread lost the CPU race (a_journaled_input_wedge\n#      subscribed=false; job_escape_e2e tree-reap timeout). The two-phase split fixes\n#      THIS: when Phase B runs, Phase A is done and the box is idle.\n#\n# The test-group then serializes the heavy class WITHIN Phase B so failure mode 1\n# can't bite either, even on the idle box. max-threads = 1 is contention-free and\n# deterministic (a cap > 1 still lets two real brokers race = lucky, not reliable).\n# Group membership == the <HEAVY> phase filter. (Driver-pacing in inject_control_wedge\n# additionally removes that gate's self-inflicted CPU storm — a correctness nicety on\n# top of the structural split.)\n[test-groups]\nheavy-broker-pty = { max-threads = 1 }\n\n# <HEAVY> — the heavy real-broker/PTY/daemon-tree class, by package+binary. Swept by\n# concrete evidence: a binary is HEAVY iff some test in it spawns a real PTY child\n# (a `SpawnReq`/`spawn_program` with a `cat`/`findstr`/`yes`/`sleep`/`ping`/FLOOD\n# program) OR a real `spt daemon`/brain process tree (`supervise_brain`, CARGO_BIN_EXE\n# daemon run). Pure in-process loopback-QUIC wire tests (net_broker with NO PTY child:\n# netbroker, netstream, mesh, sync, replicate, presence, …) are LIGHT and stay in\n# Phase A. All spt-term test binaries forkpty a real PTY child, so the whole package\n# is heavy. ONE deliberate exception to the sweep criterion: `net_worker_starve` is\n# loopback-QUIC (no PTY/daemon tree) but its <300ms scheduling CANARY is the point of\n# the test — on a loaded Phase-A pool the canary reads box saturation, not worker\n# starvation (F-039 rider, doyle-ruled: isolate the rig, NEVER loosen the bound — a\n# loosened canary stops being a canary). It rides Phase B's quiet box, serialized by\n# the group. Keep this string IDENTICAL to both -E filters in ci.yml.\n#\n# FLAKE-LEDGER #15 (W4 gate, doyle RCA 2026-07-15): four spt e2e binaries that spawn a\n# REAL `spt daemon run` tree (CARGO_BIN_EXE daemon run + ~10s bringup) sat in the LIGHT\n# Phase-A pool — a 16-way daemon storm on kitsubito blew the 30s brain-readiness\n# deadline probabilistically. Six PRECONDITION brain-never-came-up sightings across the\n# W3+W4 gates (two independent runs), ROTATING membership (the load-flake tell);\n# run_no_dup_session SOLO on the warm tree = PASS in 10.7s; no OOM/fork/TasksMax events\n# on box (v6-first DNS with no default v6 route noted as a boot pad — the\n# broken-IPv6-iroh tell). All four QUALIFY as HEAVY under this file's own sweep\n# criterion and postdate the last sweep: bind_honest_cross_perch_e2e,\n# live_adapt_translation_swap_e2e, multi_subnet_bringup_e2e, run_no_dup_session_e2e —\n# added to all three <HEAVY> strings. Plus daemon_refresh_e2e, NEW in this same W4\n# branch: it spawns the identical real `spt daemon run` + endpoint-bringup tree, so it\n# qualifies at birth — classed heavy NOW rather than after its first Phase-A flake.\n# (translate_proof stays Phase A: one sighting, single-child spawn, watching.\n# composite_e2e, also new in W4, stays LIGHT: daemonless apply, no daemon tree.)\n#\n# PROJECT-INDEX W2 (todlando 2026-07-15): projindex_writer_e2e is NEW and spawns a\n# real `spt daemon run` tree TWICE (cold + warm start legs) — HEAVY AT BIRTH per the\n# FLAKE-LEDGER #15 ruling (classify at file creation, not at first flake). Added to\n# all three <HEAVY> strings.\n#\n# PROJECT-INDEX W3 (todlando 2026-07-15): projindex_reader_e2e is NEW and spawns a\n# real `spt daemon run` tree (the daemon-maintained-index reader legs) — HEAVY AT\n# BIRTH, same ruling. Added to all three <HEAVY> strings.\n[[profile.default.overrides]]\nfilter = '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|run_no_dup_session_e2e)$/)) | (package(spt-term) & kind(tes…\ntest-group = 'heavy-broker-pty'\n\n# FLAKE-LEDGER #14 (W-3, todlando 2026-07-06): the rc real-broker UNIT tests inside the\n# `spt` BINARY escape the integration-binary HEAVY filter above — they are `kind(bin)`\n# (the binary's own `#[cfg(test)]` module), not `kind(test)` — so they ran in the LIGHT\n# Phase-A pool at full parallelism. `rc::tests::attach_viewport_reconnects_across_a_broker_bounce`\n# (a real broker + a broker-bounce reconnect) then TIMED OUT @240s under a 360-way\n# `-p spt --bin spt` run; 1/1 ISOLATED green in ~1.3s — the 4th distinct full-parallel\n# trip, so the remedy is RECIPE hardening (serialize the class), not the test. Fold the\n# rc broker/bounce class into the same heavy group so it can never be starved again;\n# EXTEND this name set on each new sighting (the FLAKE-LEDGER discipline).\n[[profile.default.overrides]]\nfilter = 'package(spt) & kind(bin) & test(/^rc::tests::(attach_viewport_reconnects_across_a_broker_bounce|broker_host_child|reconnect_class_only_severed_transport_redrives)$/)'\ntest-group = 'heavy-broker-pty'\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"# cargo-nextest configuration.\r\n#\r\n# CI runs the workspace test suite under `cargo nextest run` in TWO PHASES (see the\r\n# `test` job in .github/workflows/ci.yml):\r\n#   Phase A  `-E 'not ( <HEAVY> )'`  — the light pool, full parallelism, fast.\r\n#   Phase B  `-E '<HEAVY>'`          — the heavy real-broker/PTY/daemon-tree class,\r\n#                                      run AFTER Phase A so the box is QUIET, and\r\n#                                      serialized by the test-group below.\r\n# <HEAVY> is the SAME filter string in both CI phases AND in the test-group override\r\n# at the bottom of this file — keep all three in sync (a clean partition: A is the\r\n# exact negation of B, so nothing is dropped or double-run).\r\n# See docs/RELEASE-RUNBOOK + ci-kitsubito.\r\n\r\n[profile.default]\r\n# A hung test (e.g. a blocking PTY/socket read that never completes) must FAIL FAST,\r\n# never wedge the CI job up to its wall-clock timeout. Warn once a test exceeds 60s,\r\n# then SIGKILL + fail it after 4 such periods (4 minutes). No legitimate test in this\r\n# workspace runs that long; this is purely the no-infinite-wedge backstop.\r\nslow-timeout = { period = \"60s\", terminate-after = 4 }\r\n\r\n# Concurrency cap for the heavy real-broker / PTY / daemon-tree integration tests.\r\n#\r\n# WHY: nextest fans a phase into ONE parallel pool sized to the runner's core count.\r\n# The \"heavy\" tests each spin a REAL broker, forkpty a REAL PTY child, and/or spawn a\r\n# REAL `spt daemon`/brain process tree. Two failure modes were observed on the shared\r\n# self-hosted runners:\r\n#   1. heavy-vs-heavy contention: ~16 real brokers launching at once contend for the\r\n#      scheduler / PTY allocation / socket setup, producing wait-deadline / PTY-spawn\r\n#      / child-reap TIMING flakes whose victim rotates run to run (spawn_env on Linux\r\n#      forkpty, g4 on Windows, controller_writer, attach_survives, …).\r\n#   2. heavy-starved-by-light: even with the heavy class serialized, the ~1200 LIGHT\r\n#      tests still ran at full parallelism in the SAME pool and saturated the box, so\r\n#      a heavy test's helper thread lost the CPU race (a_journaled_input_wedge\r\n#      subscribed=false; job_escape_e2e tree-reap timeout). The two-phase split fixes\r\n#      THIS: when Phase B runs, Phase A is done and the box is idle.\r\n#\r\n# The test-group then serializes the heavy class WITHIN Phase B so failure mode 1\r\n# can't bite either, even on the idle box. max-threads = 1 is contention-free and\r\n# deterministic (a cap > 1 still lets two real brokers race = lucky, not reliable).\r\n# Group membership == the <HEAVY> phase filter. (Driver-pacing in inject_control_wedge\r\n# additionally removes that gate's self-inflicted CPU storm — a correctness nicety on\r\n# top of the structural split.)\r\n[test-groups]\r\nheavy-broker-pty = { max-threads = 1 }\r\n\r\n# <HEAVY> — the heavy real-broker/PTY/daemon-tree class, by package+binary. Swept by\r\n# concrete evidence: a binary is HEAVY iff some test in it spawns a real PTY child\r\n# (a `SpawnReq`/`spawn_program` with a `cat`/`findstr`/`yes`/`sleep`/`ping`/FLOOD\r\n# program) OR a real `spt daemon`/brain process tree (`supervise_brain`, CARGO_BIN_EXE\r\n# daemon run). Pure in-process loopback-QUIC wire tests (net_broker with NO PTY child:\r\n# netbroker, netstream, mesh, sync, replicate, presence, …) are LIGHT and stay in\r\n# Phase A. All spt-term test binaries forkpty a real PTY child, so the whole package\r\n# is heavy. ONE deliberate exception to the sweep criterion: `net_worker_starve` is\r\n# loopback-QUIC (no PTY/daemon tree) but its <300ms scheduling CANARY is the point of\r\n# the test — on a loaded Phase-A pool the canary reads box saturation, not worker\r\n# starvation (F-039 rider, doyle-ruled: isolate the rig, NEVER loosen the bound — a\r\n# loosened canary stops being a canary). It rides Phase B's quiet box, serialized by\r\n# the group. Keep this string IDENTICAL to both -E filters in ci.yml.\r\n#\r\n# FLAKE-LEDGER #15 (W4 gate, doyle RCA 2026-07-15): four spt e2e binaries that spawn a\r\n# REAL `spt daemon run` tree (CARGO_BIN_EXE daemon run + ~10s bringup) sat in the LIGHT\r\n# Phase-A pool — a 16-way daemon storm on kitsubito blew the 30s brain-readiness\r\n# deadline probabilistically. Six PRECONDITION brain-never-came-up sightings across the\r\n# W3+W4 gates (two independent runs), ROTATING membership (the load-flake tell);\r\n# run_no_dup_session SOLO on the warm tree = PASS in 10.7s; no OOM/fork/TasksMax events\r\n# on box (v6-first DNS with no default v6 route noted as a boot pad — the\r\n# broken-IPv6-iroh tell). All four QUALIFY as HEAVY under this file's own sweep\r\n# criterion and postdate the last sweep: bind_honest_cross_perch_e2e,\r\n# live_adapt_translation_swap_e2e, multi_subnet_bringup_e2e, run_no_dup_session_e2e —\r\n# added to all three <HEAVY> strings. Plus daemon_refresh_e2e, NEW in this same W4\r\n# branch: it spawns the identical real `spt daemon run` + endpoint-bringup tree, so it\r\n# qualifies at birth — classed heavy NOW rather than after its first Phase-A flake.\r\n# (translate_proof stays Phase A: one sighting, single-child spawn, watching.\r\n# composite_e2e, also new in W4, stays LIGHT: daemonless apply, no daemon tree.)\r\n#\r\n# PROJECT-INDEX W2 (todlando 2026-07-15): projindex_writer_e2e is NEW and spawns a\r\n# real `spt daemon run` tree TWICE (cold + warm start legs) — HEAVY AT BIRTH per the\r\n# FLAKE-LEDGER #15 ruling (classify at file creation, not at first flake). Added to\r\n# all three <HEAVY> strings.\r\n#\r\n# PROJECT-INDEX W3 (todlando 2026-07-15): projindex_reader_e2e is NEW and spawns a\r\n# real `spt daemon run` tree (the daemon-maintained-index reader legs) — HEAVY AT\r\n# BIRTH, same ruling. Added to all three <HEAVY> strings.\r\n[[profile.default.overrides]]\r\nfilter = '(package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|run_no_dup_session_e2e)$/)) | (package(spt-term) & kind(tes…\ntest-group = 'heavy-broker-pty'\r\n\r\n# FLAKE-LEDGER #14 (W-3, todlando 2026-07-06): the rc real-broker UNIT tests inside the\r\n# `spt` BINARY escape the integration-binary HEAVY filter above — they are `kind(bin)`\r\n# (the binary's own `#[cfg(test)]` module), not `kind(test)` — so they ran in the LIGHT\r\n# Phase-A pool at full parallelism. `rc::tests::attach_viewport_reconnects_across_a_broker_bounce`\r\n# (a real broker + a broker-bounce reconnect) then TIMED OUT @240s under a 360-way\r\n# `-p spt --bin spt` run; 1/1 ISOLATED green in ~1.3s — the 4th distinct full-parallel\r\n# trip, so the remedy is RECIPE hardening (serialize the class), not the test. Fold the\r\n# rc broker/bounce class into the same heavy group so it can never be starved again;\r\n# EXTEND this name set on each new sighting (the FLAKE-LEDGER discipline).\r\n[[profile.default.overrides]]\r\nfilter = 'package(spt) & kind(bin) & test(/^rc::tests::(attach_viewport_reconnects_across_a_broker_bounce|broker_host_child|reconnect_class_only_severed_transport_redrives)$/)'\r\ntest-group = 'heavy-broker-pty'\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.config\\nextest.toml"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195819795}}
{"type":"message","id":"d37c339b","parentId":"687b3df4","timestamp":"2026-07-16T09:56:59.799Z","message":{"role":"toolResult","toolCallId":"call_cCP8wNPZ4hy6NWHrLx4ttKHN|fc_0b42584a8ab0545d016a58aaebbd0c8199a9bf4b3fbd2c6dc1","toolName":"grep","content":[{"type":"text","text":"# crates/spt/tests/\n## daemon_refresh_e2e.rs\n 200|\n*201|    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\n*202|    let harness_alive_after = harness_pid\n*203|        .map(spt_store::proc::is_process_alive)\n 204|        .unwrap_or(false);\n 205|    let mut still_online = false;\n 206|    let online_deadline = Instant::now() + Duration::from_secs(10);\n...\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 221|         --- refresh stderr ---\\n{refresh_stderr}\\n=== endpoint run stderr ===\\n{run_stderr}\\n\\\n 222|         === brain stderr ===\\n{brain_stderr}\",\n...\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## brain_survive.rs\n 9|//!\n*10|//! No single test proves the whole claim; three legs do, and this file is honest\n 11|//! only when read alongside the other two:\n 12|//!\n 13|//! - **Leg (a) — the real-broker-process leg: `crates/spt/tests/brain_split.rs`.**\n...\n 28|//! DEFERRED upgrade: once the live-agent adapter makes daemon-hosted sessions a\n*29|//! production reality, a full-stack single-process E2E (the real `spt daemon run`\n 30|//! hosting *and* surviving) becomes reachable — tracked in `docs/DEFERRED.md`,\n 31|//! NOT D7 work.\n 32|//!\n...\n 50|//!\n*51|//! **Held AND functional (doyle F1).** Proving the endpoints are *held* (pid /\n*52|//! session_count / conn_count unchanged) is not proving they are *alive* — a\n 53|//! zombie conn or wedged PTY would pass. So after the swap the test also drives a\n 54|//! marker through the held PTY child and watches the broker's output seq advance:\n 55|//! the full round-trip (input → child → echo → broker ring) works through the\n...\n 194|    let home = tempfile::tempdir().expect(\"tempdir\");\n*195|    // Single-test binary by design — this is the SOLE `#[test]` here, so it owns\n 196|    // the process-global SPT_HOME on its own thread (cross-binary tests are\n 197|    // separate processes). A SECOND `#[test]` added to this file MUST bring the\n 198|    // env-lock discipline (a shared mutex / serialized home), or it will race this\n...\n 245|        // Both endpoints are now broker-held.\n*246|        assert_eq!(broker.session_count(), 1, \"one hosted session established\");\n 247|        assert_eq!(broker.net_conn_count(), 1, \"one live QUIC conn established\");\n 248|        (sid, child_pid)\n 249|        // driver dropped here — HARD detach (no graceful handoff frame).\n...\n 337|    );\n*338|    assert_eq!(broker.session_count(), 1, \"exactly one hosted session throughout\");\n 339|    assert_eq!(\n 340|        broker.net_conn_count(),\n 341|        1,\n...\n 344|\n*345|    // ── FUNCTIONAL survival (doyle F1, plan choreography 5(d)): \"held\" is not\n*346|    //    \"alive\". Drive a marker through the held PTY child POST-swap and watch the\n 347|    //    broker-side output seq advance — proving the full round-trip (input →\n 348|    //    child → echo → broker ring) works through the swapped-in brain, not that\n 349|    //    a zombie handle merely lingers. The input rides the KIND_INPUT effect verb\n*350|    //    on a FRESH raw connection, which does NOT steal the supervised brain's\n 351|    //    output subscription (it is not an attach). ──\n 352|    let seq_before = broker\n 353|        .session_output_seq(sid)\n*354|        .unwrap_or_else(|| teardown_panic(&stop, \"hosted session vanished before the functional probe\"));\n 355|    {\n 356|        let mut raw = LocalSocketTransport::connect(&broker_socket_name())\n 357|            .unwrap_or_else(|_| teardown_panic(&stop, \"could not open a raw input connection\"));\n...\n 361|            session_id: sid,\n*362|            data_b64: encode_bytes(b\"D7-1 POST-SWAP FUNCTIONAL PROBE\\r\"),\n 363|            op_id: None,\n 364|            minter: None,\n 365|            ack: true,\n## resume_no_control_steal_e2e.rs\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 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...\n 69|//!\n*70|//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\n 71|//! to doyle 2026-07-09). Failure B is the daemon-brain-conn SATURATION symptom; in an\n 72|//! in-proc broker a local `rc` (by:None) `same_identity`-TAKES at the broker even in the RED\n*73|//! state, so it does not discriminate here, and forcing real subprocess-brain-conn\n 74|//! saturation is brittle (the same nondeterminism the counter-54 rig engineered around). We\n 75|//! assert the ROOT (the steal) that CAUSES B; a full-daemon B repro is left to PR CI /\n 76|//! field verification per doyle's ruling (heavy E2Es off the local gate).\n 77|//!\n*78|//! ## RUNNER CONTRACT — single `#[test]`, process-per-test (nextest) or -j1\n 79|//!\n 80|//! The SOLE `#[test]` here by design: it owns the process-global `SPT_HOME` (the subprocess\n 81|//! brain derives `broker_socket_name()` from it — the brain_survive.rs / trial_drain_drive\n## attach_wedge_e2e.rs\n 21|//! residual loopback write. This test is the faithful real-daemon proof that the wedge\n*22|//! does not occur — and folds in L3 (dead endpoint offlined within one\n 23|//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).\n 24|//!\n 25|//! All spawned pids are reaped SCOPED (never machine-wide — shared CI runner).\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();"}],"details":{"scopePath":"crates/spt/tests/daemon_refresh_e2e.rs, crates/spt/tests/brain_survive.rs, crates/spt/tests/resume_no_control_steal_e2e.rs, crates/spt/tests/attach_wedge_e2e.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":26,"fileCount":4,"files":["crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/brain_survive.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/attach_wedge_e2e.rs"],"fileMatches":[{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":6},{"path":"crates/spt/tests/brain_survive.rs","count":12},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":6},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":2}],"truncated":false,"displayContent":"# crates/spt/tests/\n## daemon_refresh_e2e.rs\n 200│\n*201│    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\n*202│    let harness_alive_after = harness_pid\n*203│        .map(spt_store::proc::is_process_alive)\n 204│        .unwrap_or(false);\n 205│    let mut still_online = false;\n 206│    let online_deadline = Instant::now() + Duration::from_secs(10);\n    │...\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 221│         --- refresh stderr ---\\n{refresh_stderr}\\n=== endpoint run stderr ===\\n{run_stderr}\\n\\\n 222│         === brain stderr ===\\n{brain_stderr}\",\n    │...\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## brain_survive.rs\n   9│//!\n *10│//! No single test proves the whole claim; three legs do, and this file is honest\n  11│//! only when read alongside the other two:\n  12│//!\n  13│//! - **Leg (a) — the real-broker-process leg: `crates/spt/tests/brain_split.rs`.**\n    │...\n  28│//! DEFERRED upgrade: once the live-agent adapter makes daemon-hosted sessions a\n *29│//! production reality, a full-stack single-process E2E (the real `spt daemon run`\n  30│//! hosting *and* surviving) becomes reachable — tracked in `docs/DEFERRED.md`,\n  31│//! NOT D7 work.\n  32│//!\n    │...\n  50│//!\n *51│//! **Held AND functional (doyle F1).** Proving the endpoints are *held* (pid /\n *52│//! session_count / conn_count unchanged) is not proving they are *alive* — a\n  53│//! zombie conn or wedged PTY would pass. So after the swap the test also drives a\n  54│//! marker through the held PTY child and watches the broker's output seq advance:\n  55│//! the full round-trip (input → child → echo → broker ring) works through the\n    │...\n 194│    let home = tempfile::tempdir().expect(\"tempdir\");\n*195│    // Single-test binary by design — this is the SOLE `#[test]` here, so it owns\n 196│    // the process-global SPT_HOME on its own thread (cross-binary tests are\n 197│    // separate processes). A SECOND `#[test]` added to this file MUST bring the\n 198│    // env-lock discipline (a shared mutex / serialized home), or it will race this\n    │...\n 245│        // Both endpoints are now broker-held.\n*246│        assert_eq!(broker.session_count(), 1, \"one hosted session established\");\n 247│        assert_eq!(broker.net_conn_count(), 1, \"one live QUIC conn established\");\n 248│        (sid, child_pid)\n 249│        // driver dropped here — HARD detach (no graceful handoff frame).\n    │...\n 337│    );\n*338│    assert_eq!(broker.session_count(), 1, \"exactly one hosted session throughout\");\n 339│    assert_eq!(\n 340│        broker.net_conn_count(),\n 341│        1,\n    │...\n 344│\n*345│    // ── FUNCTIONAL survival (doyle F1, plan choreography 5(d)): \"held\" is not\n*346│    //    \"alive\". Drive a marker through the held PTY child POST-swap and watch the\n 347│    //    broker-side output seq advance — proving the full round-trip (input →\n 348│    //    child → echo → broker ring) works through the swapped-in brain, not that\n 349│    //    a zombie handle merely lingers. The input rides the KIND_INPUT effect verb\n*350│    //    on a FRESH raw connection, which does NOT steal the supervised brain's\n 351│    //    output subscription (it is not an attach). ──\n 352│    let seq_before = broker\n 353│        .session_output_seq(sid)\n*354│        .unwrap_or_else(|| teardown_panic(&stop, \"hosted session vanished before the functional probe\"));\n 355│    {\n 356│        let mut raw = LocalSocketTransport::connect(&broker_socket_name())\n 357│            .unwrap_or_else(|_| teardown_panic(&stop, \"could not open a raw input connection\"));\n    │...\n 361│            session_id: sid,\n*362│            data_b64: encode_bytes(b\"D7-1 POST-SWAP FUNCTIONAL PROBE\\r\"),\n 363│            op_id: None,\n 364│            minter: None,\n 365│            ack: true,\n## resume_no_control_steal_e2e.rs\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 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   │...\n 69│//!\n*70│//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\n 71│//! to doyle 2026-07-09). Failure B is the daemon-brain-conn SATURATION symptom; in an\n 72│//! in-proc broker a local `rc` (by:None) `same_identity`-TAKES at the broker even in the RED\n*73│//! state, so it does not discriminate here, and forcing real subprocess-brain-conn\n 74│//! saturation is brittle (the same nondeterminism the counter-54 rig engineered around). We\n 75│//! assert the ROOT (the steal) that CAUSES B; a full-daemon B repro is left to PR CI /\n 76│//! field verification per doyle's ruling (heavy E2Es off the local gate).\n 77│//!\n*78│//! ## RUNNER CONTRACT — single `#[test]`, process-per-test (nextest) or -j1\n 79│//!\n 80│//! The SOLE `#[test]` here by design: it owns the process-global `SPT_HOME` (the subprocess\n 81│//! brain derives `broker_socket_name()` from it — the brain_survive.rs / trial_drain_drive\n## attach_wedge_e2e.rs\n  21│//! residual loopback write. This test is the faithful real-daemon proof that the wedge\n *22│//! does not occur — and folds in L3 (dead endpoint offlined within one\n  23│//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).\n  24│//!\n  25│//! All spawned pids are reaped SCOPED (never machine-wide — shared CI runner).\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();"},"isError":false,"timestamp":1784195819799}}
{"type":"message","id":"23f6fa87","parentId":"d37c339b","timestamp":"2026-07-16T09:56:59.801Z","message":{"role":"toolResult","toolCallId":"call_trPBxiTfYFmTcxHqcADA6R2I|fc_0b42584a8ab0545d016a58aaebbd008199b4c42167026ca351","toolName":"grep","content":[{"type":"text","text":"# crates/spt/\n\n## src/\n### cli.rs\n 4145|    /// Peer-pump last-tick wall-clock ms (`None` = no heartbeat recorded).\n*4146|    pump_heartbeat_ms: Option<u64>,\n 4147|    /// The managing service label, if a registered service owns this daemon.\n 4148|    managed_by: Option<String>,\n 4149|    managed_active: Option<bool>,\n...\n 4318|            .then(|| broker_image.as_deref() != Some(env!(\"CARGO_PKG_VERSION\")));\n*4319|        let pump_heartbeat_ms = if running {\n 4320|            spt_daemon::read_pump_heartbeat()\n 4321|        } else {\n 4322|            None\n...\n 4369|            net_up,\n*4370|            pump_heartbeat_ms,\n 4371|            managed_by,\n 4372|            managed_active,\n 4373|            subnets,\n\n## tests/\n### attach_wedge_e2e.rs\n 114|\n*115|/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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...\n 141|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*142|                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 143|                        let _ = tx.send(true);\n 144|                        break;\n 145|                    }\n### dummy_harness_e2e.rs\n 229|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*230|                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 231|                        let _ = tx.send(acc.clone());\n 232|                        break;\n 233|                    }\n...\n 240|    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();\n*241|    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\n 242|\n 243|    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n 244|    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();\n...\n 313|        rc_saw_tick,\n*314|        \"`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \\\n 315|         output (got {} bytes) — the broker must forward an already-producing harness's \\\n 316|         PTY to the attach without the replay-vs-forward deadlock.\\n=== rc stderr ===\\n{rc_stderr}\\\n 317|         \\n=== brain stderr ===\\n{brain_stderr}\",\n...\n 333|// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\n*334|// DUMMY_HARNESS_TICK — proof the attach waited for bringup and connected to the\n 335|// live session — exactly as the --start + separate `rc` path above does.\n 336|//\n 337|// Pre-fix this test is RED: the captured output shows the offline/no-live-session\n...\n 493|    //    a tick, in BOTH pre- and post-fix runs). Stream stdout in a thread until\n*494|    //    DUMMY_HARNESS_TICK (the heartbeat-delivery keystone), exactly as the sister\n 495|    //    test's `rc` attach does. ──\n 496|    let run_err = home.path().join(\"run.stderr.log\");\n 497|    let run_err_file = std::fs::File::create(&run_err).expect(\"create endpoint-run stderr log\");\n...\n 517|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*518|                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 519|                        let _ = tx.send(acc.clone());\n 520|                        break;\n 521|                    }\n...\n 532|    // harness's live heartbeat — the SAME live-PTY marker the sister test asserts.\n*533|    let saw_tick = run_captured.contains(\"DUMMY_HARNESS_TICK\");\n 534|    let run_stderr_txt = std::fs::read_to_string(&run_err).unwrap_or_default();\n 535|    // Diagnostic discriminators for the pre-fix race losses (info only).\n 536|    let bailed_offline = run_captured.contains(\"offline — nothing to attach\")\n...\n 623|         offline failfast for the pre-bind window), and DELIVER its \\\n*624|         live DUMMY_HARNESS_TICK — instead the immediate attach read the still-OFFLINE status \\\n 625|         and bailed (bailed_offline={bailed_offline} bailed_no_session={bailed_no_session}, \\\n 626|         {} stdout bytes, no heartbeat).\\n=== endpoint run stdout ===\\n{run_captured}\\\n 627|         \\n=== endpoint run stderr ===\\n{run_stderr_txt}\\n=== brain stderr ===\\n{brain_stderr}\",\n### multi_subnet_bringup_e2e.rs\n 405|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*406|                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 407|                        let _ = tx.send(acc.clone());\n 408|                        break;\n 409|                    }\n...\n 416|    let rc_captured = rx.recv_timeout(Duration::from_secs(12)).unwrap_or_default();\n*417|    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\n 418|\n 419|    // Re-read status AFTER the attach: it must STILL be UNBOUND — proving the attach\n 420|    // landed on a genuinely pre-bind session, never a bound one."}],"details":{"scopePath":"Cargo.toml, crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":15,"fileCount":4,"files":["crates/spt/src/cli.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/tests/multi_subnet_bringup_e2e.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":3},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":2},{"path":"crates/spt/tests/dummy_harness_e2e.rs","count":8},{"path":"crates/spt/tests/multi_subnet_bringup_e2e.rs","count":2}],"truncated":false,"displayContent":"# crates/spt/\n\n## src/\n### cli.rs\n 4145│    /// Peer-pump last-tick wall-clock ms (`None` = no heartbeat recorded).\n*4146│    pump_heartbeat_ms: Option<u64>,\n 4147│    /// The managing service label, if a registered service owns this daemon.\n 4148│    managed_by: Option<String>,\n 4149│    managed_active: Option<bool>,\n     │...\n 4318│            .then(|| broker_image.as_deref() != Some(env!(\"CARGO_PKG_VERSION\")));\n*4319│        let pump_heartbeat_ms = if running {\n 4320│            spt_daemon::read_pump_heartbeat()\n 4321│        } else {\n 4322│            None\n     │...\n 4369│            net_up,\n*4370│            pump_heartbeat_ms,\n 4371│            managed_by,\n 4372│            managed_active,\n 4373│            subnets,\n\n## tests/\n### attach_wedge_e2e.rs\n 114│\n*115│/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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    │...\n 141│                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*142│                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 143│                        let _ = tx.send(true);\n 144│                        break;\n 145│                    }\n### dummy_harness_e2e.rs\n 229│                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*230│                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 231│                        let _ = tx.send(acc.clone());\n 232│                        break;\n 233│                    }\n    │...\n 240│    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();\n*241│    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\n 242│\n 243│    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n 244│    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();\n    │...\n 313│        rc_saw_tick,\n*314│        \"`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \\\n 315│         output (got {} bytes) — the broker must forward an already-producing harness's \\\n 316│         PTY to the attach without the replay-vs-forward deadlock.\\n=== rc stderr ===\\n{rc_stderr}\\\n 317│         \\n=== brain stderr ===\\n{brain_stderr}\",\n    │...\n 333│// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\n*334│// DUMMY_HARNESS_TICK — proof the attach waited for bringup and connected to the\n 335│// live session — exactly as the --start + separate `rc` path above does.\n 336│//\n 337│// Pre-fix this test is RED: the captured output shows the offline/no-live-session\n    │...\n 493│    //    a tick, in BOTH pre- and post-fix runs). Stream stdout in a thread until\n*494│    //    DUMMY_HARNESS_TICK (the heartbeat-delivery keystone), exactly as the sister\n 495│    //    test's `rc` attach does. ──\n 496│    let run_err = home.path().join(\"run.stderr.log\");\n 497│    let run_err_file = std::fs::File::create(&run_err).expect(\"create endpoint-run stderr log\");\n    │...\n 517│                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*518│                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 519│                        let _ = tx.send(acc.clone());\n 520│                        break;\n 521│                    }\n    │...\n 532│    // harness's live heartbeat — the SAME live-PTY marker the sister test asserts.\n*533│    let saw_tick = run_captured.contains(\"DUMMY_HARNESS_TICK\");\n 534│    let run_stderr_txt = std::fs::read_to_string(&run_err).unwrap_or_default();\n 535│    // Diagnostic discriminators for the pre-fix race losses (info only).\n 536│    let bailed_offline = run_captured.contains(\"offline — nothing to attach\")\n    │...\n 623│         offline failfast for the pre-bind window), and DELIVER its \\\n*624│         live DUMMY_HARNESS_TICK — instead the immediate attach read the still-OFFLINE status \\\n 625│         and bailed (bailed_offline={bailed_offline} bailed_no_session={bailed_no_session}, \\\n 626│         {} stdout bytes, no heartbeat).\\n=== endpoint run stdout ===\\n{run_captured}\\\n 627│         \\n=== endpoint run stderr ===\\n{run_stderr_txt}\\n=== brain stderr ===\\n{brain_stderr}\",\n### multi_subnet_bringup_e2e.rs\n 405│                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*406│                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 407│                        let _ = tx.send(acc.clone());\n 408│                        break;\n 409│                    }\n    │...\n 416│    let rc_captured = rx.recv_timeout(Duration::from_secs(12)).unwrap_or_default();\n*417│    let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");\n 418│\n 419│    // Re-read status AFTER the attach: it must STILL be UNBOUND — proving the attach\n 420│    // landed on a genuinely pre-bind session, never a bound one."},"isError":false,"timestamp":1784195819801}}
{"type":"message","id":"9d831646","parentId":"23f6fa87","timestamp":"2026-07-16T09:56:59.806Z","message":{"role":"toolResult","toolCallId":"call_PjtBkWMTM8zPSfktrz5SV4HD|fc_0b42584a8ab0545d016a58aaebbd1881998a2bfea7e494f084","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/tests/\n### bootstrap_verb_e2e.rs\n 5|//! at the canonical `<SPT_HOME>/bin` path, is idempotent on re-run, and the\n*6|//! PLACED binary's `spt update fetch` speaks the gh channel — proven by the\n 7|//! loud `GhCliRequired` refusal when no `gh` is resolvable, which is the gh\n 8|//! carrier gate itself, not an HTTPS fallback.\n 9|//!\n...\n 86|        .output()\n*87|        .expect(\"run placed spt update fetch\");\n 88|    let stderr = String::from_utf8_lossy(&fetch.stderr);\n 89|    assert!(!fetch.status.success(), \"fetch without gh must refuse\");\n 90|    assert!(\n### brain_respawn_rename.rs\n 122|    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\n*123|    //    what `spt update apply` does (rename-then-replace). ──\n 124|    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\n 125|    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\n 126|    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\n### brain_survive.rs\n 311|    *selected.lock().unwrap() = fixture_b.clone();\n*312|    restart.request();\n 313|\n 314|    // ── Generation 1: the supervisor respawned the brain PROCESS onto the\n 315|    //    SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). ──\n### composite_e2e.rs\n*1|//! THE-FORKENING W4 — the bare `spt update` composite, end to end\n 2|//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\n 3|//! release, THEN updates a registered gh_release adapter — core-first order,\n 4|//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\n...\n 51|    rx.recv_timeout(deadline)\n*52|        .expect(\"spt update must complete within its deadline\")\n*53|        .expect(\"run spt update\")\n 54|}\n 55|\n 56|/// The signed update set the fake channel serves: one artifact for the CURRENT\n...\n 175|\n*176|    // ── (4) ONE invocation: bare `spt update`. ──\n 177|    let out = {\n 178|        let mut cmd = Command::new(&spt);\n 179|        cmd.no_window()\n...\n 196|        out.status.success(),\n*197|        \"bare `spt update` must exit 0 when both legs succeed\"\n 198|    );\n 199|\n 200|    // ── (5) CORE leg landed: the private spt copy now holds the NEW bytes\n...\n 215|        stderr.contains(\"ADAPTER_UPDATE_DONE:cc\"),\n*216|        \"the adapter update must apply: {stderr}\"\n 217|    );\n 218|    assert!(\n 219|        stderr.contains(\"ADAPTER_UPDATE_SUMMARY:cc: updated 1.0.0 -> 1.1.0\"),\n### daemon_refresh_e2e.rs\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 6|//! verb. The recovery story: wedged brain-held state no longer needs a full\n 7|//! daemon bounce that kills every PTY.\n 8|//!\n### docs_bundle_e2e.rs\n 2|//! (ADR-0036 §4): a staged set whose SIGNED metadata carries the docs entry\n*3|//! lands version-matched docs at `$SPT_HOME/docs` on `spt update apply`; a\n 4|//! corrupted staged bundle degrades to the machine-greppable\n*5|//! `UPDATE_DOCS_SKIPPED` on stderr while the BINARY apply outcome is\n 6|//! byte-for-byte unchanged (doyle pre-ruling 2026-07-14: assert both).\n 7|//!\n 8|//! Hermetic: a temp `SPT_HOME` scopes the socket namespace (no daemon serves\n...\n 104|/// Stage set + artifact (+ optionally docs bytes) into `home`'s cache, then\n*105|/// run the real `spt update apply` against that home. Returns (exit ok,\n 106|/// stdout, stderr, swapped binary bytes).\n 107|fn stage_and_apply(\n 108|    home: &Path,\n...\n 128|    let out = Command::new(spt)\n*129|        .args([\"update\", \"apply\"])\n 130|        .env(\"SPT_HOME\", home)\n 131|        .output()\n*132|        .expect(\"run spt update apply\");\n 133|    // The swap targets the RUNNING exe — read back the driven copy itself.\n 134|    let swapped = std::fs::read(spt).ok();\n 135|    (\n### live_adapt_translation_swap_e2e.rs\n 376|        apply_result.is_ok(),\n*377|        \"REQ-ADAPTER-LIVE-UPDATE: Brain::adapter_apply must return Ok (the broker stopped \\\n 378|         the resident translation child, CRC-swapped the install dir, and restarted the \\\n 379|         translation) — got {:?}.\\n=== brain stderr ===\\n{brain_stderr}\",\n 380|        apply_result.err().map(|e| e.to_string())\n...\n 385|        swap_landed,\n*386|        \"REQ-ADAPTER-LIVE-UPDATE: after adapter_apply the install-dir translation binary \\\n 387|         {} must hold the NEW (staged) bytes — the daemon-coordinated swap must land on \\\n 388|         the previously-locked .exe. hash_before={hash_before:?} hash_after={hash_after:?} \\\n 389|         hash_staged={hash_staged:?}.\\n=== brain stderr ===\\n{brain_stderr}\",\n### release_verify_e2e.rs\n*1|//! M6-D7 acceptance rung — the `spt update` dry path against the REAL\n 2|//! published release: download the live `*.release.json` SignedRelease assets\n 3|//! over the **gh carrier** (ADR-0036: the release channel is private, so the\n 4|//! fetch rides an org-authenticated `gh release download` — exactly the\n*5|//! avenue a fielded node's `spt update fetch` uses) and verify them against\n 6|//! the **embedded** two-key trust anchor, exactly as a deployed node would\n 7|//! judge a propagated release. Proves the whole REQ-REL-2 chain end to end —\n 8|//! tag-triggered pipeline → asset set → manual local signing → a binary in\n### resume_no_control_steal_e2e.rs\n 9|//!\n*10|//! `spt update fetch --apply` (v0.30.2 → v0.30.3) PROMOTED CLEANLY (the counter-54 fix\n 11|//! worked) but under 7 live spt-hosted broker PTYs it FROZE all 7 + failed every `spt rc`\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### trial_drain_drive_e2e.rs\n 13|//!\n*14|//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n 15|//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n 16|//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n 17|//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n\n## spt-daemon/tests/\n### brain_swap.rs\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 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 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### 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### endpoint_survival.rs\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### false_promote.rs\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### propagate.rs\n 723|\n*724|    // Round 3: the operator ran `spt update apply` — converged.\n 725|    lab.cache.record_applied(target).unwrap();\n 726|    lab.cache.record_last_outcome(\"applied\").unwrap();\n 727|    let (report, _) = status_pull(&coord, &lab, &trust);\n### replicate.rs\n 30|};\n*31|use spt_net::net::replicate::{apply_update, LineDecoder, RegistryUpdate};\n 32|use spt_proto::identity::Identity;\n 33|use spt_store::epoch::EpochSource;\n 34|use spt_store::visibility::VisibilityStore;\n...\n 222|                for upd in decoder.push(&bytes) {\n*223|                    outcomes.push(apply_update(&mut b_regs, upd).expect(\"member subnet\"));\n 224|                }\n 225|            }\n 226|            BrokerEvent::NetStreamEof { stream_id, .. } if stream_id == stream => break,"}],"details":{"scopePath":"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":33,"fileCount":16,"files":["crates/spt/tests/bootstrap_verb_e2e.rs","crates/spt/tests/brain_respawn_rename.rs","crates/spt/tests/brain_survive.rs","crates/spt/tests/composite_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt/tests/live_adapt_translation_swap_e2e.rs","crates/spt/tests/release_verify_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/propagate.rs","crates/spt-daemon/tests/replicate.rs"],"fileMatches":[{"path":"crates/spt/tests/bootstrap_verb_e2e.rs","count":2},{"path":"crates/spt/tests/brain_respawn_rename.rs","count":1},{"path":"crates/spt/tests/brain_survive.rs","count":1},{"path":"crates/spt/tests/composite_e2e.rs","count":6},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":1},{"path":"crates/spt/tests/docs_bundle_e2e.rs","count":5},{"path":"crates/spt/tests/live_adapt_translation_swap_e2e.rs","count":2},{"path":"crates/spt/tests/release_verify_e2e.rs","count":2},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":1},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/brain_swap.rs","count":4},{"path":"crates/spt-daemon/tests/docs_server_e2e.rs","count":2},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":1},{"path":"crates/spt-daemon/tests/false_promote.rs","count":1},{"path":"crates/spt-daemon/tests/propagate.rs","count":1},{"path":"crates/spt-daemon/tests/replicate.rs","count":2}],"truncated":false,"displayContent":"# crates/\n\n## spt/tests/\n### bootstrap_verb_e2e.rs\n  5│//! at the canonical `<SPT_HOME>/bin` path, is idempotent on re-run, and the\n *6│//! PLACED binary's `spt update fetch` speaks the gh channel — proven by the\n  7│//! loud `GhCliRequired` refusal when no `gh` is resolvable, which is the gh\n  8│//! carrier gate itself, not an HTTPS fallback.\n  9│//!\n   │...\n 86│        .output()\n*87│        .expect(\"run placed spt update fetch\");\n 88│    let stderr = String::from_utf8_lossy(&fetch.stderr);\n 89│    assert!(!fetch.status.success(), \"fetch without gh must refuse\");\n 90│    assert!(\n### brain_respawn_rename.rs\n 122│    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\n*123│    //    what `spt update apply` does (rename-then-replace). ──\n 124│    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\n 125│    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\n 126│    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\n### brain_survive.rs\n 311│    *selected.lock().unwrap() = fixture_b.clone();\n*312│    restart.request();\n 313│\n 314│    // ── Generation 1: the supervisor respawned the brain PROCESS onto the\n 315│    //    SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). ──\n### composite_e2e.rs\n  *1│//! THE-FORKENING W4 — the bare `spt update` composite, end to end\n   2│//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core\n   3│//! release, THEN updates a registered gh_release adapter — core-first order,\n   4│//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a\n    │...\n  51│    rx.recv_timeout(deadline)\n *52│        .expect(\"spt update must complete within its deadline\")\n *53│        .expect(\"run spt update\")\n  54│}\n  55│\n  56│/// The signed update set the fake channel serves: one artifact for the CURRENT\n    │...\n 175│\n*176│    // ── (4) ONE invocation: bare `spt update`. ──\n 177│    let out = {\n 178│        let mut cmd = Command::new(&spt);\n 179│        cmd.no_window()\n    │...\n 196│        out.status.success(),\n*197│        \"bare `spt update` must exit 0 when both legs succeed\"\n 198│    );\n 199│\n 200│    // ── (5) CORE leg landed: the private spt copy now holds the NEW bytes\n    │...\n 215│        stderr.contains(\"ADAPTER_UPDATE_DONE:cc\"),\n*216│        \"the adapter update must apply: {stderr}\"\n 217│    );\n 218│    assert!(\n 219│        stderr.contains(\"ADAPTER_UPDATE_SUMMARY:cc: updated 1.0.0 -> 1.1.0\"),\n### daemon_refresh_e2e.rs\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 6│//! verb. The recovery story: wedged brain-held state no longer needs a full\n 7│//! daemon bounce that kills every PTY.\n 8│//!\n### docs_bundle_e2e.rs\n   2│//! (ADR-0036 §4): a staged set whose SIGNED metadata carries the docs entry\n  *3│//! lands version-matched docs at `$SPT_HOME/docs` on `spt update apply`; a\n   4│//! corrupted staged bundle degrades to the machine-greppable\n  *5│//! `UPDATE_DOCS_SKIPPED` on stderr while the BINARY apply outcome is\n   6│//! byte-for-byte unchanged (doyle pre-ruling 2026-07-14: assert both).\n   7│//!\n   8│//! Hermetic: a temp `SPT_HOME` scopes the socket namespace (no daemon serves\n    │...\n 104│/// Stage set + artifact (+ optionally docs bytes) into `home`'s cache, then\n*105│/// run the real `spt update apply` against that home. Returns (exit ok,\n 106│/// stdout, stderr, swapped binary bytes).\n 107│fn stage_and_apply(\n 108│    home: &Path,\n    │...\n 128│    let out = Command::new(spt)\n*129│        .args([\"update\", \"apply\"])\n 130│        .env(\"SPT_HOME\", home)\n 131│        .output()\n*132│        .expect(\"run spt update apply\");\n 133│    // The swap targets the RUNNING exe — read back the driven copy itself.\n 134│    let swapped = std::fs::read(spt).ok();\n 135│    (\n### live_adapt_translation_swap_e2e.rs\n 376│        apply_result.is_ok(),\n*377│        \"REQ-ADAPTER-LIVE-UPDATE: Brain::adapter_apply must return Ok (the broker stopped \\\n 378│         the resident translation child, CRC-swapped the install dir, and restarted the \\\n 379│         translation) — got {:?}.\\n=== brain stderr ===\\n{brain_stderr}\",\n 380│        apply_result.err().map(|e| e.to_string())\n    │...\n 385│        swap_landed,\n*386│        \"REQ-ADAPTER-LIVE-UPDATE: after adapter_apply the install-dir translation binary \\\n 387│         {} must hold the NEW (staged) bytes — the daemon-coordinated swap must land on \\\n 388│         the previously-locked .exe. hash_before={hash_before:?} hash_after={hash_after:?} \\\n 389│         hash_staged={hash_staged:?}.\\n=== brain stderr ===\\n{brain_stderr}\",\n### release_verify_e2e.rs\n*1│//! M6-D7 acceptance rung — the `spt update` dry path against the REAL\n 2│//! published release: download the live `*.release.json` SignedRelease assets\n 3│//! over the **gh carrier** (ADR-0036: the release channel is private, so the\n 4│//! fetch rides an org-authenticated `gh release download` — exactly the\n*5│//! avenue a fielded node's `spt update fetch` uses) and verify them against\n 6│//! the **embedded** two-key trust anchor, exactly as a deployed node would\n 7│//! judge a propagated release. Proves the whole REQ-REL-2 chain end to end —\n 8│//! tag-triggered pipeline → asset set → manual local signing → a binary in\n### resume_no_control_steal_e2e.rs\n  9│//!\n*10│//! `spt update fetch --apply` (v0.30.2 → v0.30.3) PROMOTED CLEANLY (the counter-54 fix\n 11│//! worked) but under 7 live spt-hosted broker PTYs it FROZE all 7 + failed every `spt rc`\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### trial_drain_drive_e2e.rs\n 13│//!\n*14│//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n 15│//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n 16│//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n 17│//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n\n## spt-daemon/tests/\n### brain_swap.rs\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  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 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### 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### endpoint_survival.rs\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### false_promote.rs\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### propagate.rs\n 723│\n*724│    // Round 3: the operator ran `spt update apply` — converged.\n 725│    lab.cache.record_applied(target).unwrap();\n 726│    lab.cache.record_last_outcome(\"applied\").unwrap();\n 727│    let (report, _) = status_pull(&coord, &lab, &trust);\n### replicate.rs\n  30│};\n *31│use spt_net::net::replicate::{apply_update, LineDecoder, RegistryUpdate};\n  32│use spt_proto::identity::Identity;\n  33│use spt_store::epoch::EpochSource;\n  34│use spt_store::visibility::VisibilityStore;\n    │...\n 222│                for upd in decoder.push(&bytes) {\n*223│                    outcomes.push(apply_update(&mut b_regs, upd).expect(\"member subnet\"));\n 224│                }\n 225│            }\n 226│            BrokerEvent::NetStreamEof { stream_id, .. } if stream_id == stream => break,"},"isError":false,"timestamp":1784195819806}}
{"type":"message","id":"a134b505","parentId":"9d831646","timestamp":"2026-07-16T09:57:07.698Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning workspace crate search**"},{"type":"toolCall","id":"call_2bSoB3U8RM31SNb2HROfk5rb|fc_0b42584a8ab0545d016a58aaf3a6f8819990f446bece09787a","name":"grep","arguments":{"i":"Finding mock adapter package","pattern":"mock-adapter|mock-session|DUMMY_HARNESS_TICK","path":".","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_qPi2J8B3OTEpMOcjiw5svjp5|fc_0b42584a8ab0545d016a58aaf3a710819986cdda7dd2522ca2","name":"glob","arguments":{"i":"Mapping fixture packages","path":"fixtures/**/*;test-fixtures/**/*;examples/**/*;**/src/bin/mock*.rs","hidden":true,"gitignore":true,"limit":300}},{"type":"toolCall","id":"call_fsimDkxsn6PR5uoPF5wCEnEd|fc_0b42584a8ab0545d016a58aaf3a7188199af5b3a4cf0f912fb","name":"read","arguments":{"i":"Reading refresh implementation seam","path":"crates/spt-daemon/src/applyhost.rs","selector":"320-375,780-855"}},{"type":"toolCall","id":"call_b63gE2EF4Z2zrx57Sjmg24zf|fc_0b42584a8ab0545d016a58aaf3a720819986340f543bd8d390","name":"read","arguments":{"i":"Reading brain boot resume seam","path":"crates/spt-daemon/src/brainproc.rs","selector":"170-295"}},{"type":"toolCall","id":"call_unWHzT7bTbsKfZqPrv9Z11S6|fc_0b42584a8ab0545d016a58aaf3a72c819996bef6662aba1179","name":"read","arguments":{"i":"Reading controller attach semantics","path":"crates/spt-daemon/src/broker.rs","selector":"5450-5570"}},{"type":"toolCall","id":"call_qQn74vWnzusYhHi3YPFcpqsk|fc_0b42584a8ab0545d016a58aaf3a73481998d605979c717b2c2","name":"read","arguments":{"i":"Reading update ADR rules","path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","selector":"1-90"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14399,"output":321,"cacheRead":188928,"cacheWrite":0,"totalTokens":203648,"reasoningTokens":37,"cost":{"input":0.071995,"output":0.00963,"cacheRead":0.09446399999999999,"cacheWrite":0,"total":0.176089}},"stopReason":"toolUse","timestamp":1784195819860,"responseId":"resp_0b42584a8ab0545d016a58aaec43748199b45ef1705c274311","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aaedd738819993fa45dbfdf26338","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrzKYczTq-vi5ZdpzEtiFzQEP9-GxRKVDrxSQ23kWdNGHi0zyqXi8imJSQxnlTHQfFxihbUIwkhegBzENmut6OZWC4R2-24iUeIdDeOWK4AQFNqYvwpQJQftuJhcBHCwwZPeczo5ioTgVbrJ163sZ08VBiGpxaREc4R1WUYZeaQJaOcNtvL4_0heu-E9-5GkrpMWt-X5httd-naZATvw0eN-IqVgQCR-Lds1ko9LiBO2GpswvVIQga5oXU0aF1226wFWn1zHBWKbhQT8Azckz-Nrlin1vd1cw_SyiqXDb57Fwk_Xe0RHfsBddJgARCnLWn0WqzNogr48v1cBMBc6Me0n5z1YjZmRuBjJJ-kM8WL9b3jyhdZposee-D0UXvHB_DNfRQ24c2KdGDV8htW7w71Q7lff6w6wQLGub13YhoqjF8jTRstGcsIWDmSnS89lVAoeeihZdMUF82d2yl9lNXdKHrl6A-w-mGveXc6wPOnrMZzYuwTze7qrsYH_IGr8z6tcp_KdOjY9K5KfD8Am9dcrZsIfGYlWQcxpdjRuU29MGWXBhXaDt9k2SoyIvbN6absFtOfOx9gVvL4FxvBldotiCcKJEj6F4vbqY-wRjPwS9a-SJ51j5NWGdNcmrgPSpr7nBRutvmccfo1T2FmuUZfc5iSpz-PKmQD0XCNlnqffruiiiMlgsWaG1d4P-q1SGjqervLfdUTdMWygTimKdn06Ms7A2OgYhb8yhdqWumJPMidxtq9QFaYROJScI-t3vf6r7OVTpjw_Ss3RLB72YPInW8VqQvqvVuX6NBfdZ6kTThJZbkQ1DkM6B8aYL-IoU0oDIVtbV3lcKoUtetUdKGTwB7YJcW9VKMRpC-V8kx0EH6iuVVBk5Ix3td4eCzv_fxOsyvY03aHW-UY2I1BxcVOEeFlKU4etJk8qvIjf1CCSc3l_dVPHVJIU-kVGKVX7DZa-Wi3qrSo0C1RFC2-TAatGNEny2uQflyni3HJsyllFoC7rM_G_7cAtHkdR-MnDmQ2oAwM5N7whZrEMR9aNVDOuFWavkFY1qVukt_lncAn9yewgZ-27sUIpTtvCqGVii6hnQUO-BgXV26jyRYfXAT6aNZHEKYs9-dBXRAZV5m2ZrM=","summary":[{"type":"summary_text","text":"**Planning workspace crate search**"}]},{"id":"fc_0b42584a8ab0545d016a58aaf3a6f8819990f446bece09787a","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding mock adapter package\",\"pattern\":\"mock-adapter|mock-session|DUMMY_HARNESS_TICK\",\"path\":\".\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_2bSoB3U8RM31SNb2HROfk5rb","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58aaf3a710819986cdda7dd2522ca2","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping fixture packages\",\"path\":\"fixtures/**/*;test-fixtures/**/*;examples/**/*;**/src/bin/mock*.rs\",\"hidden\":true,\"gitignore\":true,\"limit\":300}","call_id":"call_qPi2J8B3OTEpMOcjiw5svjp5","name":"glob"},{"id":"fc_0b42584a8ab0545d016a58aaf3a7188199af5b3a4cf0f912fb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh implementation seam\",\"path\":\"crates/spt-daemon/src/applyhost.rs\",\"selector\":\"320-375,780-855\"}","call_id":"call_fsimDkxsn6PR5uoPF5wCEnEd","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaf3a720819986340f543bd8d390","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain boot resume seam\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"170-295\"}","call_id":"call_b63gE2EF4Z2zrx57Sjmg24zf","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaf3a72c819996bef6662aba1179","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controller attach semantics\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"5450-5570\"}","call_id":"call_unWHzT7bTbsKfZqPrv9Z11S6","name":"read"},{"id":"fc_0b42584a8ab0545d016a58aaf3a73481998d605979c717b2c2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update ADR rules\",\"path\":\"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md\",\"selector\":\"1-90\"}","call_id":"call_qQn74vWnzusYhHi3YPFcpqsk","name":"read"}]},"duration":7836.460200000554,"ttft":2000.6698000002652,"contextSnapshot":{"promptTokens":203327,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_2bSoB3U8RM31SNb2HROfk5rb|fc_0b42584a8ab0545d016a58aaf3a6f8819990f446bece09787a","toolName":"grep","startedAt":"2026-07-16T09:57:07.699Z","args":{"path":"."},"intent":"Finding mock adapter package"},"id":"822ef999","parentId":"a134b505","timestamp":"2026-07-16T09:57:07.699Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qPi2J8B3OTEpMOcjiw5svjp5|fc_0b42584a8ab0545d016a58aaf3a710819986cdda7dd2522ca2","toolName":"glob","startedAt":"2026-07-16T09:57:07.699Z","args":{"path":"fixtures/**/*;test-fixtures/**/*;examples/**/*;**/src/bin/mock*.rs"},"intent":"Mapping fixture packages"},"id":"f8a791ef","parentId":"822ef999","timestamp":"2026-07-16T09:57:07.699Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fsimDkxsn6PR5uoPF5wCEnEd|fc_0b42584a8ab0545d016a58aaf3a7188199af5b3a4cf0f912fb","toolName":"read","startedAt":"2026-07-16T09:57:07.699Z","args":{"path":"crates/spt-daemon/src/applyhost.rs"},"intent":"Reading refresh implementation seam"},"id":"103996ac","parentId":"f8a791ef","timestamp":"2026-07-16T09:57:07.699Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_b63gE2EF4Z2zrx57Sjmg24zf|fc_0b42584a8ab0545d016a58aaf3a720819986340f543bd8d390","toolName":"read","startedAt":"2026-07-16T09:57:07.699Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain boot resume seam"},"id":"5606b4af","parentId":"103996ac","timestamp":"2026-07-16T09:57:07.699Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_unWHzT7bTbsKfZqPrv9Z11S6|fc_0b42584a8ab0545d016a58aaf3a72c819996bef6662aba1179","toolName":"read","startedAt":"2026-07-16T09:57:07.699Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading controller attach semantics"},"id":"5f4de963","parentId":"5606b4af","timestamp":"2026-07-16T09:57:07.699Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qQn74vWnzusYhHi3YPFcpqsk|fc_0b42584a8ab0545d016a58aaf3a73481998d605979c717b2c2","toolName":"read","startedAt":"2026-07-16T09:57:07.699Z","args":{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md"},"intent":"Reading update ADR rules"},"id":"bd3d2f91","parentId":"5f4de963","timestamp":"2026-07-16T09:57:07.699Z"}
{"type":"message","id":"d8352de1","parentId":"bd3d2f91","timestamp":"2026-07-16T09:57:07.710Z","message":{"role":"toolResult","toolCallId":"call_fsimDkxsn6PR5uoPF5wCEnEd|fc_0b42584a8ab0545d016a58aaf3a7188199af5b3a4cf0f912fb","toolName":"read","content":[{"type":"text","text":"pub fn apply_staged(\n…\n) -> Result<ApplyStagedOutcome, String> {\n…\n    // brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not\n    // attach to any session (the broker holds every hosted endpoint across the\n    // brain cycle — that is the whole point).\n    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n\n    swap_and_record(cache, exe_path, version, &artifact)?;\n\n    // REQ-UPD-3's moment (ADR-0018 D3-3): trigger a real brain-PROCESS restart.\n    // The broker signals its supervisor to cycle the brain child onto the\n    // just-swapped binary with `start-reason=update`; new code runs immediately,\n    // no manual bounce, every broker-held endpoint untouched at the process\n    // level. This replaces the old in-process `apply_brain_only` handoff (the\n    // regression: it re-attached a CLI-side subscriber while the live brain kept\n    // running the OLD code). `classify` already gated this as brain-only, so the\n    // broker never moves; only the brain restarts.\n    //\n    // A broker without a supervisor wired (`honored: false`) still swapped the\n    // binary — the next daemon start uses the new executable. Readiness-gated\n    // promotion + auto-rollback on a brain that fails to come up is D6.\n    match brain.request_brain_restart() {\n        Ok(_honored) => Ok(ApplyStagedOutcome::AppliedPending {\n            version,\n            product_version,\n        }),\n        Err(e) => Err(format!(\n            \"binary swapped to v{version}, but the brain-restart trigger failed: {e} — the broker \\\n             and every hosted endpoint are intact; retry `spt update apply` (an older broker that \\\n             predates the restart verb needs a one-time manual daemon restart to run the new code)\"\n        )),\n    }\n}\n\n/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n/// broker and raise the same supervisor brain-restart signal the post-swap\n/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n/// construction. The recovery verb for wedged brain-held state that previously\n/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\n/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n/// mirrors the apply path: `false` means the broker has no supervisor wired\n/// (accepted, not acted on).\n// [impl->REQ-DAEMON-REFRESH]\npub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n    brain\n        .request_brain_restart()\n        .map_err(|e| format!(\"brain-restart trigger: {e}\"))\n}\n\n/// Apply the staged release with NO daemon running (REQ-UPDATE-ONE-SHOT-FINISH):\n/// swap the binary + record the trial, and touch NO broker. This is the fix for\n/// the operator wart where `update apply` on a stopped box booted the OLD broker\n/// pre-swap (`ensure_daemon_announced`) just to hand off to it — guaranteeing a\n/// mixed old-broker/new-brain pair and a manual bounce. Here nothing is booted:\n…\n    fn apply_staged_refuses_platform_blind_or_mismatched_single() {\n…\n        cache\n            .stage_single_with_platform(&signed, b\"CROSS-OS BYTES\", current_platform())\n            .unwrap();\n        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n        assert!(\n            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n            \"matching-platform single applies, got {out:?}\"\n        );\n        assert_eq!(std::fs::read(&exe).unwrap(), b\"CROSS-OS BYTES\");\n    }\n\n    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] [unit->REQ-UPD-3] the planned\n    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n    // supervisor — the test/no-daemon shape) accepts the verb but acks\n    // `honored: false`. This is the broker half of the apply trigger that\n    // replaces the in-process handoff.\n    #[test]\n    fn brain_restart_verb_raises_the_supervisor_signal() {\n        let name = unique_name();\n        let broker = served_broker(&name);\n\n        // No signal wired: accepted, not honored.\n        let mut b = cold_connect_retry(&name);\n        assert!(\n            !b.request_brain_restart().expect(\"verb round-trips\"),\n            \"a broker with no supervisor wired must ack honored=false\"\n        );\n\n        // Wire the supervisor signal; the verb now raises it and reports honored.\n        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n        let mut b2 = cold_connect_retry(&name);\n        assert!(\n            b2.request_brain_restart().expect(\"verb round-trips\"),\n            \"a broker with a supervisor wired must ack honored=true\"\n        );\n        assert!(\n            signal.take(),\n            \"the verb must raise the planned-restart signal the supervisor consumes\"\n        );\n    }\n\n    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n    // staged release, NO release cache, NO binary swap. The verb has no staging\n    // preconditions: the signal raises and `honored` reports the supervisor\n    // wiring truthfully, with the executable untouched.\n    #[test]\n    fn refresh_brain_cycles_without_staging_or_swap_preconditions() {\n        let name = unique_name();\n        let broker = served_broker(&name);\n\n        // No supervisor wired: the refresh round-trips and reports not-honored.\n        assert!(\n            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n            \"no supervisor wired must report honored=false\"\n        );\n\n        // Supervisor wired: refresh raises the same planned-restart signal the\n        // apply path rides — with nothing staged anywhere.\n        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n        assert!(\n            refresh_brain(&name).expect(\"refresh round-trips\"),\n            \"a wired supervisor must report honored=true\"\n        );\n        assert!(\n            signal.take(),\n            \"refresh must raise the planned-restart signal the supervisor consumes\"\n        );\n    }\n\n    fn echo_spawn_req() -> SpawnReq {\n        #[cfg(unix)]\n        let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n…\n    }"}],"details":{"displayContent":{"text":"pub fn apply_staged(\n…\n) -> Result<ApplyStagedOutcome, String> {\n…\n    // brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not\n    // attach to any session (the broker holds every hosted endpoint across the\n    // brain cycle — that is the whole point).\n    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n\n    swap_and_record(cache, exe_path, version, &artifact)?;\n\n    // REQ-UPD-3's moment (ADR-0018 D3-3): trigger a real brain-PROCESS restart.\n    // The broker signals its supervisor to cycle the brain child onto the\n    // just-swapped binary with `start-reason=update`; new code runs immediately,\n    // no manual bounce, every broker-held endpoint untouched at the process\n    // level. This replaces the old in-process `apply_brain_only` handoff (the\n    // regression: it re-attached a CLI-side subscriber while the live brain kept\n    // running the OLD code). `classify` already gated this as brain-only, so the\n    // broker never moves; only the brain restarts.\n    //\n    // A broker without a supervisor wired (`honored: false`) still swapped the\n    // binary — the next daemon start uses the new executable. Readiness-gated\n    // promotion + auto-rollback on a brain that fails to come up is D6.\n    match brain.request_brain_restart() {\n        Ok(_honored) => Ok(ApplyStagedOutcome::AppliedPending {\n            version,\n            product_version,\n        }),\n        Err(e) => Err(format!(\n            \"binary swapped to v{version}, but the brain-restart trigger failed: {e} — the broker \\\n             and every hosted endpoint are intact; retry `spt update apply` (an older broker that \\\n             predates the restart verb needs a one-time manual daemon restart to run the new code)\"\n        )),\n    }\n}\n\n/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n/// broker and raise the same supervisor brain-restart signal the post-swap\n/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n/// construction. The recovery verb for wedged brain-held state that previously\n/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\n/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n/// mirrors the apply path: `false` means the broker has no supervisor wired\n/// (accepted, not acted on).\n// [impl->REQ-DAEMON-REFRESH]\npub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n    brain\n        .request_brain_restart()\n        .map_err(|e| format!(\"brain-restart trigger: {e}\"))\n}\n\n/// Apply the staged release with NO daemon running (REQ-UPDATE-ONE-SHOT-FINISH):\n/// swap the binary + record the trial, and touch NO broker. This is the fix for\n/// the operator wart where `update apply` on a stopped box booted the OLD broker\n/// pre-swap (`ensure_daemon_announced`) just to hand off to it — guaranteeing a\n/// mixed old-broker/new-brain pair and a manual bounce. Here nothing is booted:\n…\n    fn apply_staged_refuses_platform_blind_or_mismatched_single() {\n…\n        cache\n            .stage_single_with_platform(&signed, b\"CROSS-OS BYTES\", current_platform())\n            .unwrap();\n        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n        assert!(\n            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n            \"matching-platform single applies, got {out:?}\"\n        );\n        assert_eq!(std::fs::read(&exe).unwrap(), b\"CROSS-OS BYTES\");\n    }\n\n    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] [unit->REQ-UPD-3] the planned\n    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n    // supervisor — the test/no-daemon shape) accepts the verb but acks\n    // `honored: false`. This is the broker half of the apply trigger that\n    // replaces the in-process handoff.\n    #[test]\n    fn brain_restart_verb_raises_the_supervisor_signal() {\n        let name = unique_name();\n        let broker = served_broker(&name);\n\n        // No signal wired: accepted, not honored.\n        let mut b = cold_connect_retry(&name);\n        assert!(\n            !b.request_brain_restart().expect(\"verb round-trips\"),\n            \"a broker with no supervisor wired must ack honored=false\"\n        );\n\n        // Wire the supervisor signal; the verb now raises it and reports honored.\n        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n        let mut b2 = cold_connect_retry(&name);\n        assert!(\n            b2.request_brain_restart().expect(\"verb round-trips\"),\n            \"a broker with a supervisor wired must ack honored=true\"\n        );\n        assert!(\n            signal.take(),\n            \"the verb must raise the planned-restart signal the supervisor consumes\"\n        );\n    }\n\n    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n    // staged release, NO release cache, NO binary swap. The verb has no staging\n    // preconditions: the signal raises and `honored` reports the supervisor\n    // wiring truthfully, with the executable untouched.\n    #[test]\n    fn refresh_brain_cycles_without_staging_or_swap_preconditions() {\n        let name = unique_name();\n        let broker = served_broker(&name);\n\n        // No supervisor wired: the refresh round-trips and reports not-honored.\n        assert!(\n            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n            \"no supervisor wired must report honored=false\"\n        );\n\n        // Supervisor wired: refresh raises the same planned-restart signal the\n        // apply path rides — with nothing staged anywhere.\n        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n        assert!(\n            refresh_brain(&name).expect(\"refresh round-trips\"),\n            \"a wired supervisor must report honored=true\"\n        );\n        assert!(\n            signal.take(),\n            \"refresh must raise the planned-restart signal the supervisor consumes\"\n        );\n    }\n\n    fn echo_spawn_req() -> SpawnReq {\n        #[cfg(unix)]\n        let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n…\n    }","startLine":303,"lineNumbers":[303,null,308,null,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,739,null,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,null,870]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\applyhost.rs"}}},"isError":false,"timestamp":1784195827710}}
{"type":"message","id":"84765d98","parentId":"d8352de1","timestamp":"2026-07-16T09:57:07.711Z","message":{"role":"toolResult","toolCallId":"call_qPi2J8B3OTEpMOcjiw5svjp5|fc_0b42584a8ab0545d016a58aaf3a710819986cdda7dd2522ca2","toolName":"glob","content":[{"type":"text","text":"No files found matching pattern\nSkipped missing paths: fixtures/**/*, test-fixtures/**/*, examples/**/*"}],"details":{"scopePath":".","fileCount":0,"files":[],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","missingPaths":["fixtures/**/*","test-fixtures/**/*","examples/**/*"]},"isError":false,"useless":true,"timestamp":1784195827711}}
{"type":"message","id":"11523458","parentId":"84765d98","timestamp":"2026-07-16T09:57:07.715Z","message":{"role":"toolResult","toolCallId":"call_qQn74vWnzusYhHi3YPFcpqsk|fc_0b42584a8ab0545d016a58aaf3a73481998d605979c717b2c2","toolName":"read","content":[{"type":"text","text":"# Single consolidated daemon with broker/brain split; peer-propagated gated self-update\n\n## Status\n\naccepted (2026-05-29)\n\n## Context\n\nADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:\n\n1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\n\n2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\n\n## Decision\n\n**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.\n\n**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:\n- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.\n- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.\n\nRoutine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.\n\nRejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).\n\n**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\n\n**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n\n## Consequences\n\n- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n- Peer-propagated updates make release signing mandatory, not optional.\n- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n\n## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n\nCodex adversarial review (`docs/reviews/STAGE-A-codex-redteam.md`) put 4 FATAL findings on this ADR; Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`, `spt-spikes/spike-01-broker-handoff`) tested the hardest path. Resulting binding amendments:\n\n### A. Update-class taxonomy (resolves FATAL #1 — R-UPD-3 self-contradiction)\nR-UPD-3's \"no endpoint terminates during update\" is **only** absolute for the *brain-only* class. Three classes, each with its own invariant:\n- **brain-only** (the routine case) — endpoints MUST survive untouched. **Spike #1: PROVEN on Windows ConPTY.**\n- **broker-compatible** — broker binary swap behind a versioned IPC the running broker can hot-accept; endpoints survive; no spike yet.\n- **broker-breaking** — held-resource-type or OS-API change; requires a *planned endpoint-cycle*. R-UPD-3 is explicitly weakened here: this class MAY suspend endpoints, with consent + scheduling. PRD R-UPD-3 must be reworded to scope the absolute guarantee to brain-only.\n\n### B. Ownership table (resolves FATAL #4 — \"any live stream owned by brain breaks the invariant\")\nThe boundary is normative, not \"broker holds fds, brain holds logic.\" A resource is broker-owned **iff** a live consumer would lose continuity on brain restart.\n\n| Resource | Owner | Spiked? |\n|---|---|---|\n| PTY master read+write handle | **broker** | ✅ #1 |\n| Spawned harness child process | **broker** | ✅ #1 |\n| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |\n| Iroh endpoint + accepted QUIC streams | **broker** | ✅ #3 (loopback shape) → **implemented M4-D4a** (`spt-daemon::nethost::NetHost`: dedicated tokio runtime in the broker owns the endpoint + conn table; brain drives status/dial over IPC net frames) |\n| mDNS socket / relay session | **broker** | ✅ closed by construction at M4-D4a (2026-06-03): `MdnsAddressLookup` and the relay session are constructed *inside* the broker-owned iroh endpoint — there is no separate socket to own |\n| Listening sockets | **broker** | ✅ #1 |\n| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |\n\nConsequence codex forced into the open: pushing live Iroh/QUIC ownership into the broker means networking stream-state lives in the \"stable kernel,\" enlarging it. Accepted as the cost of the invariant; the broker is \"stable\" in *update cadence*, not in *narrowness*.\n\n### C. ConPTY DSR gotcha (new hazard, discovered by Spike #1)\nConPTY withholds **all** child stdout until the terminal answers its startup cursor-position query (`ESC [ 6 n`). A broker reading a ConPTY master that does not reply `ESC [ <r>;<c> R` sees only the 4-byte query and then nothing — looks like a hung/silent child. Spike got 0 bytes of program output until the reader answered DSR, then full output. → `REQ-HAZARD-CONPTY-DSR`; every ConPTY reader must auto-answer DSR. Added to KNOWN-HAZARDS §5.\n\n### D. Self-update delivery hardening (resolves SERIOUS #5)\nSignature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by \"binary signature\"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.\n\n### E. Open spike gaps (must close before M3 builds the daemon)\n1. ✅ **CLOSED (Spike #3, 2026-06-01).** Live Iroh + file-transfer stream survival across brain restart (FATAL #2). Spike #1 only proved PTY + plain TCP; Spike #3 stood up a broker-owned Iroh endpoint + uni QUIC transfer and proved a live peer download survives a brain restart gapless + exactly-once. Loopback shape validated; off-node transport + the QUIC-ownership *implementation* defer to M4. (`docs/spikes/SPIKE-03-quic-survival.md`.)\n2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\n3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-session attach yields the viewport only, not scrollback. (`docs/spikes/SPIKE-05-restart-stress.md`.)\n4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\n\n**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\n"}],"details":{"displayContent":{"text":"# Single consolidated daemon with broker/brain split; peer-propagated gated self-update\r\n\r\n## Status\r\n\r\naccepted (2026-05-29)\r\n\r\n## Context\r\n\r\nADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:\r\n\r\n1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\r\n\r\n2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\r\n\r\n## Decision\r\n\r\n**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.\r\n\r\n**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:\r\n- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.\r\n- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.\r\n\r\nRoutine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.\r\n\r\nRejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).\r\n\r\n**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\r\n\r\n**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\r\n\r\n## Consequences\r\n\r\n- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\r\n- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\r\n- Peer-propagated updates make release signing mandatory, not optional.\r\n- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\r\n- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\r\n- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\r\n\r\n## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\r\n\r\nCodex adversarial review (`docs/reviews/STAGE-A-codex-redteam.md`) put 4 FATAL findings on this ADR; Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`, `spt-spikes/spike-01-broker-handoff`) tested the hardest path. Resulting binding amendments:\r\n\r\n### A. Update-class taxonomy (resolves FATAL #1 — R-UPD-3 self-contradiction)\r\nR-UPD-3's \"no endpoint terminates during update\" is **only** absolute for the *brain-only* class. Three classes, each with its own invariant:\r\n- **brain-only** (the routine case) — endpoints MUST survive untouched. **Spike #1: PROVEN on Windows ConPTY.**\r\n- **broker-compatible** — broker binary swap behind a versioned IPC the running broker can hot-accept; endpoints survive; no spike yet.\r\n- **broker-breaking** — held-resource-type or OS-API change; requires a *planned endpoint-cycle*. R-UPD-3 is explicitly weakened here: this class MAY suspend endpoints, with consent + scheduling. PRD R-UPD-3 must be reworded to scope the absolute guarantee to brain-only.\r\n\r\n### B. Ownership table (resolves FATAL #4 — \"any live stream owned by brain breaks the invariant\")\r\nThe boundary is normative, not \"broker holds fds, brain holds logic.\" A resource is broker-owned **iff** a live consumer would lose continuity on brain restart.\r\n\r\n| Resource | Owner | Spiked? |\r\n|---|---|---|\r\n| PTY master read+write handle | **broker** | ✅ #1 |\r\n| Spawned harness child process | **broker** | ✅ #1 |\r\n| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |\r\n| Iroh endpoint + accepted QUIC streams | **broker** | ✅ #3 (loopback shape) → **implemented M4-D4a** (`spt-daemon::nethost::NetHost`: dedicated tokio runtime in the broker owns the endpoint + conn table; brain drives status/dial over IPC net frames) |\r\n| mDNS socket / relay session | **broker** | ✅ closed by construction at M4-D4a (2026-06-03): `MdnsAddressLookup` and the relay session are constructed *inside* the broker-owned iroh endpoint — there is no separate socket to own |\r\n| Listening sockets | **broker** | ✅ #1 |\r\n| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |\r\n\r\nConsequence codex forced into the open: pushing live Iroh/QUIC ownership into the broker means networking stream-state lives in the \"stable kernel,\" enlarging it. Accepted as the cost of the invariant; the broker is \"stable\" in *update cadence*, not in *narrowness*.\r\n\r\n### C. ConPTY DSR gotcha (new hazard, discovered by Spike #1)\r\nConPTY withholds **all** child stdout until the terminal answers its startup cursor-position query (`ESC [ 6 n`). A broker reading a ConPTY master that does not reply `ESC [ <r>;<c> R` sees only the 4-byte query and then nothing — looks like a hung/silent child. Spike got 0 bytes of program output until the reader answered DSR, then full output. → `REQ-HAZARD-CONPTY-DSR`; every ConPTY reader must auto-answer DSR. Added to KNOWN-HAZARDS §5.\r\n\r\n### D. Self-update delivery hardening (resolves SERIOUS #5)\r\nSignature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by \"binary signature\"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.\r\n\r\n### E. Open spike gaps (must close before M3 builds the daemon)\r\n1. ✅ **CLOSED (Spike #3, 2026-06-01).** Live Iroh + file-transfer stream survival across brain restart (FATAL #2). Spike #1 only proved PTY + plain TCP; Spike #3 stood up a broker-owned Iroh endpoint + uni QUIC transfer and proved a live peer download survives a brain restart gapless + exactly-once. Loopback shape validated; off-node transport + the QUIC-ownership *implementation* defer to M4. (`docs/spikes/SPIKE-03-quic-survival.md`.)\r\n2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\r\n3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-session attach yields the viewport only, not scrollback. (`docs/spikes/SPIKE-05-restart-stress.md`.)\r\n4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\r\n\r\n**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0004-single-daemon-broker-brain-split-and-self-update.md"}}},"isError":false,"timestamp":1784195827715}}
{"type":"message","id":"e5577376","parentId":"11523458","timestamp":"2026-07-16T09:57:07.728Z","message":{"role":"toolResult","toolCallId":"call_b63gE2EF4Z2zrx57Sjmg24zf|fc_0b42584a8ab0545d016a58aaf3a720819986340f543bd8d390","toolName":"read","content":[{"type":"text","text":"    // 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[Showing lines 169-298 of 2116. Use :299 to continue]"}],"details":{"truncation":{"content":"    // 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","truncated":true,"truncatedBy":"lines","totalLines":1948,"totalBytes":8280,"outputLines":130,"outputBytes":8280,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    // 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","startLine":169,"lineNumbers":[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-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":8280,"outputLines":130,"outputBytes":8280,"shownRange":{"start":169,"end":298},"nextOffset":299}}},"isError":false,"timestamp":1784195827728}}
{"type":"message","id":"65b8b432","parentId":"e5577376","timestamp":"2026-07-16T09:57:07.772Z","message":{"role":"toolResult","toolCallId":"call_unWHzT7bTbsKfZqPrv9Z11S6|fc_0b42584a8ab0545d016a58aaf3a72c819996bef6662aba1179","toolName":"read","content":[{"type":"text","text":"\n    /// A connected local-socket pair for the unit: bind a listener, connect a\n    /// client `Stream`, accept the server `Stream`. Returns the SERVER send half\n    /// wrapped as a `SharedSend` (the broker side a `become_controller` writes\n    /// through) plus the CLIENT `Stream` (the brain side we read the wire off) and\n    /// the unused server RECV half (held so the server fd stays open while we\n    /// read). Mirrors `LocalSocketTransport`'s own round-trip test + `handle_conn`'s\n    /// `conn.split()`.\n    fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {\n        use std::sync::atomic::AtomicU32;\n        // A process-unique pipe/UDS name so parallel test threads never collide.\n        static SEQ: AtomicU32 = AtomicU32::new(0);\n        let name = format!(\n            \"spt-daemon-p1c-{}-{}.sock\",\n            std::process::id(),\n            SEQ.fetch_add(1, Ordering::Relaxed)\n        );\n        let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");\n        let client = LocalSocketTransport::connect(&name).expect(\"connect client\");\n        let server = listener.accept().expect(\"accept server side\");\n        let (recv, send) = server.split();\n        (\n            Arc::new(BrokerConn::new(send, brain_write_deadline())),\n            client,\n            recv,\n        )\n    }\n\n    /// Read exactly `n` framed `output` envelopes off the wire and return their\n    /// `seq`s in arrival order. A blocking `read_frame` per frame — the writer\n    /// threads push them; the test runner's own timeout catches a genuine stall\n    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\n    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\n        let mut seqs = Vec::with_capacity(n);\n        while seqs.len() < n {\n            let env = read_frame(client).expect(\"output frame on the wire\");\n            if env.kind == crate::msg::KIND_OUTPUT {\n                let ev: crate::msg::OutputEvent =\n                    serde_json::from_value(env.payload).expect(\"output payload\");\n                seqs.push(ev.seq);\n            }\n        }\n        seqs\n    }\n\n    /// EPOCH-GATE KEYSTONE (REQ-HAZARD-CONTROLLER-WRITER-REORDER), post-#6. With\n    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\n    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\n    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\n    /// ahead of the surviving writer's current one. We make the two repaints carry\n    /// DISTINCT seqs so a leak is order-detectable:\n    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\n    ///     `controller_epoch`, SUPERSEDING writer-A.\n    ///\n    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\n    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\n    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\n    ///\n    /// DETERMINISM: the test holds `send.lock()` across BOTH `become_controller`\n    /// calls (and the interleaved append), so neither writer thread can acquire the\n    /// socket mutex until after the epoch is bumped. On release, writer-A — whichever\n    /// order it next wins the mutex — reads the already-bumped epoch under the lock\n    /// and returns without writing. Timing-independent (no flake).\n    ///\n    /// RED-on-revert / non-vacuity: writer-A is genuinely poised with a non-empty\n    /// initial batch [repaint@1]; ONLY the in-lock gate stops it. Neuter the\n    /// `if controller_epoch.load(Acquire) != epoch { return; }` inside\n    /// `controller_writer`'s `send.lock()` arm and writer-A flushes its stale\n    /// repaint@1; since both writers serialize on the SAME socket mutex, writer-A's\n    /// seq 1 can win the first slot → the first frame read is `1`, and\n    /// `assert_eq!(seqs, [2])` fails — exactly the `got stale seq` reorder this\n    /// hazard is about.\n    // [unit->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn() {\n        let (send, mut client, _recv) = controller_socket_pair();\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        // Two chunks in the ring (seqs 0,1); the grid now holds their screen state.\n        assert!(\n            log.append(b\"AAAA\").is_none(),\n            \"no controller yet: append seq 0 returns no controller job\"\n        );\n        assert!(\n            log.append(b\"BBBB\").is_none(),\n            \"no controller yet: append seq 1 returns no controller job\"\n        );\n\n        // Pin the conn's serialized write GATE so NO writer thread can write\n        // until both registrations (and the epoch bump) are done — the same\n        // determinism lever the old socket-mutex pin gave (round-4: the gate\n        // replaced the raw mutex; write_gated runs its epoch closure under it).\n        let guard = send.pin_gate_for_test();\n\n        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\n        log.become_controller(Arc::clone(&send), None, 0);\n        // A further chunk advances the grid to a NEW current screen (seq 2). The\n        // handoff to A's (pinned) channel is buffered; A never gets to write it.\n        assert!(\n            log.append(b\"CCCC\").is_none(),\n            \"controller channel buffers seq 2 while the socket is pinned\"\n        );\n        // writer-B: registers at next_seq-1 = 2 → initial = [repaint@2]. Bumps the\n        // epoch; writer-A is superseded. Both writers park on the pinned socket.\n        log.become_controller(Arc::clone(&send), None, 0);\n\n        // Release the socket: the writers race for the mutex. The in-lock gate makes\n        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\n        drop(guard);\n\n        let seqs = drain_output_seqs(&mut client, 1);\n        assert_eq!(\n            seqs,\n            vec![2],\n            \"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\\n             wire — a superseded writer-A flushing its stale repaint@1 would put seq \\\n             1 first (the reorder this hazard guards)\"\n        );\n\n        // No second frame leaks: writer-A is epoch-gated silent, writer-B parks on\n        // its (empty) live channel. A bounded read returns None (not a stale frame).\n…\n    }\n\n[Showing lines 5449-5573 of 6009. Use :5574 to continue]"}],"details":{"truncation":{"content":"\r\n    /// A connected local-socket pair for the unit: bind a listener, connect a\r\n    /// client `Stream`, accept the server `Stream`. Returns the SERVER send half\r\n    /// wrapped as a `SharedSend` (the broker side a `become_controller` writes\r\n    /// through) plus the CLIENT `Stream` (the brain side we read the wire off) and\r\n    /// the unused server RECV half (held so the server fd stays open while we\r\n    /// read). Mirrors `LocalSocketTransport`'s own round-trip test + `handle_conn`'s\r\n    /// `conn.split()`.\r\n    fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {\r\n        use std::sync::atomic::AtomicU32;\r\n        // A process-unique pipe/UDS name so parallel test threads never collide.\r\n        static SEQ: AtomicU32 = AtomicU32::new(0);\r\n        let name = format!(\r\n            \"spt-daemon-p1c-{}-{}.sock\",\r\n            std::process::id(),\r\n            SEQ.fetch_add(1, Ordering::Relaxed)\r\n        );\r\n        let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");\r\n        let client = LocalSocketTransport::connect(&name).expect(\"connect client\");\r\n        let server = listener.accept().expect(\"accept server side\");\r\n        let (recv, send) = server.split();\r\n        (\r\n            Arc::new(BrokerConn::new(send, brain_write_deadline())),\r\n            client,\r\n            recv,\r\n        )\r\n    }\r\n\r\n    /// Read exactly `n` framed `output` envelopes off the wire and return their\r\n    /// `seq`s in arrival order. A blocking `read_frame` per frame — the writer\r\n    /// threads push them; the test runner's own timeout catches a genuine stall\r\n    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\r\n    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\r\n        let mut seqs = Vec::with_capacity(n);\r\n        while seqs.len() < n {\r\n            let env = read_frame(client).expect(\"output frame on the wire\");\r\n            if env.kind == crate::msg::KIND_OUTPUT {\r\n                let ev: crate::msg::OutputEvent =\r\n                    serde_json::from_value(env.payload).expect(\"output payload\");\r\n                seqs.push(ev.seq);\r\n            }\r\n        }\r\n        seqs\r\n    }\r\n\r\n    /// EPOCH-GATE KEYSTONE (REQ-HAZARD-CONTROLLER-WRITER-REORDER), post-#6. With\r\n    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\r\n    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\r\n    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\r\n    /// ahead of the surviving writer's current one. We make the two repaints carry\r\n    /// DISTINCT seqs so a leak is order-detectable:\r\n    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\r\n    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\r\n    ///   - append seq 2 (grid advances) → `next_seq = 3`.\r\n    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\r\n    ///     `controller_epoch`, SUPERSEDING writer-A.\r\n    ///\r\n    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\r\n    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\r\n    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\r\n    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\r\n    ///\r\n    /// DETERMINISM: the test holds `send.lock()` across BOTH `become_controller`\r\n    /// calls (and the interleaved append), so neither writer thread can acquire the\r\n    /// socket mutex until after the epoch is bumped. On release, writer-A — whichever\r\n    /// order it next wins the mutex — reads the already-bumped epoch under the lock\r\n    /// and returns without writing. Timing-independent (no flake).\r\n    ///\r\n    /// RED-on-revert / non-vacuity: writer-A is genuinely poised with a non-empty\r\n    /// initial batch [repaint@1]; ONLY the in-lock gate stops it. Neuter the\r\n    /// `if controller_epoch.load(Acquire) != epoch { return; }` inside\r\n    /// `controller_writer`'s `send.lock()` arm and writer-A flushes its stale\r\n    /// repaint@1; since both writers serialize on the SAME socket mutex, writer-A's\r\n    /// seq 1 can win the first slot → the first frame read is `1`, and\r\n    /// `assert_eq!(seqs, [2])` fails — exactly the `got stale seq` reorder this\r\n    /// hazard is about.\r\n    // [unit->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn() {\r\n        let (send, mut client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        // Two chunks in the ring (seqs 0,1); the grid now holds their screen state.\r\n        assert!(\r\n            log.append(b\"AAAA\").is_none(),\r\n            \"no controller yet: append seq 0 returns no controller job\"\r\n        );\r\n        assert!(\r\n            log.append(b\"BBBB\").is_none(),\r\n            \"no controller yet: append seq 1 returns no controller job\"\r\n        );\r\n\r\n        // Pin the conn's serialized write GATE so NO writer thread can write\r\n        // until both registrations (and the epoch bump) are done — the same\r\n        // determinism lever the old socket-mutex pin gave (round-4: the gate\r\n        // replaced the raw mutex; write_gated runs its epoch closure under it).\r\n        let guard = send.pin_gate_for_test();\r\n\r\n        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\r\n        log.become_controller(Arc::clone(&send), None, 0);\r\n        // A further chunk advances the grid to a NEW current screen (seq 2). The\r\n        // handoff to A's (pinned) channel is buffered; A never gets to write it.\r\n        assert!(\r\n            log.append(b\"CCCC\").is_none(),\r\n            \"controller channel buffers seq 2 while the socket is pinned\"\r\n        );\r\n        // writer-B: registers at next_seq-1 = 2 → initial = [repaint@2]. Bumps the\r\n        // epoch; writer-A is superseded. Both writers park on the pinned socket.\r\n        log.become_controller(Arc::clone(&send), None, 0);\r\n\r\n        // Release the socket: the writers race for the mutex. The in-lock gate makes\r\n        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\r\n        drop(guard);\r\n\r\n        let seqs = drain_output_seqs(&mut client, 1);\r\n        assert_eq!(\r\n            seqs,\r\n            vec![2],\r\n            \"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\\r\n             wire — a superseded writer-A flushing its stale repaint@1 would put seq \\\r\n             1 first (the reorder this hazard guards)\"\r\n        );\r\n\r\n        // No second frame leaks: writer-A is epoch-gated silent, writer-B parks on\r\n        // its (empty) live channel. A bounded read returns None (not a stale frame).\r","truncated":true,"truncatedBy":"lines","totalLines":561,"totalBytes":6908,"outputLines":125,"outputBytes":6908,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n    /// A connected local-socket pair for the unit: bind a listener, connect a\r\n    /// client `Stream`, accept the server `Stream`. Returns the SERVER send half\r\n    /// wrapped as a `SharedSend` (the broker side a `become_controller` writes\r\n    /// through) plus the CLIENT `Stream` (the brain side we read the wire off) and\r\n    /// the unused server RECV half (held so the server fd stays open while we\r\n    /// read). Mirrors `LocalSocketTransport`'s own round-trip test + `handle_conn`'s\r\n    /// `conn.split()`.\r\n    fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {\r\n        use std::sync::atomic::AtomicU32;\r\n        // A process-unique pipe/UDS name so parallel test threads never collide.\r\n        static SEQ: AtomicU32 = AtomicU32::new(0);\r\n        let name = format!(\r\n            \"spt-daemon-p1c-{}-{}.sock\",\r\n            std::process::id(),\r\n            SEQ.fetch_add(1, Ordering::Relaxed)\r\n        );\r\n        let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");\r\n        let client = LocalSocketTransport::connect(&name).expect(\"connect client\");\r\n        let server = listener.accept().expect(\"accept server side\");\r\n        let (recv, send) = server.split();\r\n        (\r\n            Arc::new(BrokerConn::new(send, brain_write_deadline())),\r\n            client,\r\n            recv,\r\n        )\r\n    }\r\n\r\n    /// Read exactly `n` framed `output` envelopes off the wire and return their\r\n    /// `seq`s in arrival order. A blocking `read_frame` per frame — the writer\r\n    /// threads push them; the test runner's own timeout catches a genuine stall\r\n    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\r\n    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\r\n        let mut seqs = Vec::with_capacity(n);\r\n        while seqs.len() < n {\r\n            let env = read_frame(client).expect(\"output frame on the wire\");\r\n            if env.kind == crate::msg::KIND_OUTPUT {\r\n                let ev: crate::msg::OutputEvent =\r\n                    serde_json::from_value(env.payload).expect(\"output payload\");\r\n                seqs.push(ev.seq);\r\n            }\r\n        }\r\n        seqs\r\n    }\r\n\r\n    /// EPOCH-GATE KEYSTONE (REQ-HAZARD-CONTROLLER-WRITER-REORDER), post-#6. With\r\n    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\r\n    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\r\n    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\r\n    /// ahead of the surviving writer's current one. We make the two repaints carry\r\n    /// DISTINCT seqs so a leak is order-detectable:\r\n    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\r\n    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\r\n    ///   - append seq 2 (grid advances) → `next_seq = 3`.\r\n    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\r\n    ///     `controller_epoch`, SUPERSEDING writer-A.\r\n    ///\r\n    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\r\n    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\r\n    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\r\n    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\r\n    ///\r\n    /// DETERMINISM: the test holds `send.lock()` across BOTH `become_controller`\r\n    /// calls (and the interleaved append), so neither writer thread can acquire the\r\n    /// socket mutex until after the epoch is bumped. On release, writer-A — whichever\r\n    /// order it next wins the mutex — reads the already-bumped epoch under the lock\r\n    /// and returns without writing. Timing-independent (no flake).\r\n    ///\r\n    /// RED-on-revert / non-vacuity: writer-A is genuinely poised with a non-empty\r\n    /// initial batch [repaint@1]; ONLY the in-lock gate stops it. Neuter the\r\n    /// `if controller_epoch.load(Acquire) != epoch { return; }` inside\r\n    /// `controller_writer`'s `send.lock()` arm and writer-A flushes its stale\r\n    /// repaint@1; since both writers serialize on the SAME socket mutex, writer-A's\r\n    /// seq 1 can win the first slot → the first frame read is `1`, and\r\n    /// `assert_eq!(seqs, [2])` fails — exactly the `got stale seq` reorder this\r\n    /// hazard is about.\r\n    // [unit->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn() {\r\n        let (send, mut client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        // Two chunks in the ring (seqs 0,1); the grid now holds their screen state.\r\n        assert!(\r\n            log.append(b\"AAAA\").is_none(),\r\n            \"no controller yet: append seq 0 returns no controller job\"\r\n        );\r\n        assert!(\r\n            log.append(b\"BBBB\").is_none(),\r\n            \"no controller yet: append seq 1 returns no controller job\"\r\n        );\r\n\r\n        // Pin the conn's serialized write GATE so NO writer thread can write\r\n        // until both registrations (and the epoch bump) are done — the same\r\n        // determinism lever the old socket-mutex pin gave (round-4: the gate\r\n        // replaced the raw mutex; write_gated runs its epoch closure under it).\r\n        let guard = send.pin_gate_for_test();\r\n\r\n        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\r\n        log.become_controller(Arc::clone(&send), None, 0);\r\n        // A further chunk advances the grid to a NEW current screen (seq 2). The\r\n        // handoff to A's (pinned) channel is buffered; A never gets to write it.\r\n        assert!(\r\n            log.append(b\"CCCC\").is_none(),\r\n            \"controller channel buffers seq 2 while the socket is pinned\"\r\n        );\r\n        // writer-B: registers at next_seq-1 = 2 → initial = [repaint@2]. Bumps the\r\n        // epoch; writer-A is superseded. Both writers park on the pinned socket.\r\n        log.become_controller(Arc::clone(&send), None, 0);\r\n\r\n        // Release the socket: the writers race for the mutex. The in-lock gate makes\r\n        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\r\n        drop(guard);\r\n\r\n        let seqs = drain_output_seqs(&mut client, 1);\r\n        assert_eq!(\r\n            seqs,\r\n            vec![2],\r\n            \"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\\r\n             wire — a superseded writer-A flushing its stale repaint@1 would put seq \\\r\n             1 first (the reorder this hazard guards)\"\r\n        );\r\n\r\n        // No second frame leaks: writer-A is epoch-gated silent, writer-B parks on\r\n        // its (empty) live channel. A bounded read returns None (not a stale frame).\r\n…\n    }","startLine":5449,"lineNumbers":[5449,5450,5451,5452,5453,5454,5455,5456,5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472,5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488,5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520,5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552,5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568,5569,5570,5571,5572,5573,null,5585]},"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":6908,"outputLines":125,"outputBytes":6908,"shownRange":{"start":5449,"end":5573},"nextOffset":5574}}},"isError":false,"timestamp":1784195827771}}
{"type":"message","id":"6e4f8382","parentId":"65b8b432","timestamp":"2026-07-16T09:57:08.191Z","message":{"role":"toolResult","toolCallId":"call_2bSoB3U8RM31SNb2HROfk5rb|fc_0b42584a8ab0545d016a58aaf3a6f8819990f446bece09787a","toolName":"grep","content":[{"type":"text","text":"# CONTEXT.md\n 863|**repo topology (The Forkening, ADR-0036)**:\n*864|Development home is **`BigscreenVR/spt-bs-core`** (private); the publish target is **`BigscreenVR/spt-bs-releases`** (private) — GitHub Releases carry the binaries, the docs bundle, and the signed update-set, reached only through an org-authenticated `gh`. **`SaberMage/spt-core` and `SaberMage/spt-releases` are retained as passive mirrors** (private; the merging agent pushes every main merge + tags; Actions disabled) — full history, may diverge from the dev home only if a genuine second contributor e...\n 865|_Avoid_: treating a releases repo as a second source repo; authoring doc truth there; publishing anything to the mirrors first.\n 866|\n 867|**issue / feature tracking**:\n\n# Cargo.lock\n 2341|[[package]]\n*2342|name = \"mock-adapter\"\n 2343|version = \"0.33.0\"\n 2344|dependencies = [\n 2345| \"spt-runtime\",\n\n# M2b-PLAN.md\n 61|stale-signoff sweep); the **resume-session seam** (fresh-with-preload +\n*62|continue-existing); the `api listen` wiring that hosts all of it; mock-adapter\n 63|lifecycle templates + a LiveAgent E2E.\n 64|\n 65|**Out:**\n\n# M5-D4-PLAN.md\n 201|  slice over an in-flight run.\n*202|- `cargo build -p mock-adapter` before local `shell_e2e` iteration (no dep edge —\n 203|  stale-binary trap). Flags before variadic positionals in tests/scripts (clap trap).\n 204|- Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.\n\n# M6-PLAN.md\n 43|5. **Docs scope = Tier 1** (DOCS-STRATEGY §v0.1): mental model · human quickstart ·\n*44|   adapter quickstart (via `mock-adapter`) · **complete harness-contract vertical**\n 45|   (public manifest reference + full `spt api` reference + install-on-demand\n 46|   bootstrap pattern) · shell getting-started (`spt-shell-notify` live example) ·\n 47|   generated CLI reference · **manifest JSON Schema (new code: schemars)** · root\n...\n 65|   `SHA256SUMS`, **`SignedRelease` metadata in the exact format `spt update`\n*66|   consumes**, `manifest.schema.json`, `mock-adapter` source zip.\n 67|10. **Signing (ADR-0015):** two Ed25519 keypairs — active primary + never-used\n 68|    offline recovery; **both pubkeys embedded** in the binary's trusted set; seeds\n 69|    backed up off-machine; **manual local signing** (no CI secrets).\n*70|11. **License split:** MIT for install scripts / `mock-adapter` / docs-site\n 71|    content; binary under a short proprietary freeware license with an explicit\n 72|    **royalty-free adapter/shell development clause**.\n 73|12. **Wire-proto version** stays its own counter (R-ARCH-3) — never conflated with\n...\n 123|  mental model/overview (diagrams); human quickstart (install → two agents\n*124|  message, real values); adapter quickstart (walked via `mock-adapter`);\n 125|  harness-contract vertical complete (public manifest reference rewritten from\n 126|  `docs/MANIFEST.md` — public-safe, no sister-project references; full `spt api`\n 127|  reference; install-on-demand bootstrap pattern + per-OS snippet); shell\n\n# M9-PLAN.md\n 129|subscribes to digests, and is the T4 gate's user-backed origin. In-tree mock\n*130|gateway fixture (manifest + fake binary, the R-DOCS-2 mock-adapter pattern —\n 131|no lecturn code in spt-core, per the adapter boundary). Evidence:\n 132|`REQ-EP-<n>` `[doc, impl, unit]`.\n 133|\n\n# V0.12.1-LIFECYCLE-JIT.md\n 41|  endpoint-run session reads the wrong/empty log. GATE (dummy harness): rc attach to a LIVE endpoint-run harness RECEIVES\n*42|  its DUMMY_HARNESS_TICK within a bounded window.\n 43|\n 44|- **L1 — design GATE-PASS @5ae68f8 (doc+impl+unit landed; int = OPERATOR MANUAL ACCEPTANCE, awaiting operator real-env test).**\n 45|  Landed: CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths (daemon.rs `detached_no_inherit` + deelevate.rs token spawn),\n\n# V0.19.0-P6-SCREEN-GRID-DESIGN.md\n 78|- **Units:** vte→grid application (print/CSI/SGR/cursor/alt-screen enter-exit → cell/cursor state); `render_repaint` produces a clean paint for (a) a main-screen state, (b) an alt-screen TUI state, (c) after a resize; repaint pseudo-seq monotonicity; grid single-writer / no-lock-across-fanout.\n*79|- **Int:** a broker session driven with a scripted alt-screen byte sequence (enter alt-screen, paint, scroll, partial repaint) → a fresh attach receives a CLEAN current-screen repaint (assert no raw alt-screen-enter / no torn-chunk debris in the initial batch), then live frames continue in-order. Reuse the dummy-harness/mock-adapter fixture pattern.\n 80|- **[twohost]:** NOT needed — this is single-host attach-render (the grid is server-side, the wire is unchanged; #4 already owns the cross-node int). A single-host int suffices.\n 81|- **Docs:** ADR-0031 (companion: vte dep rationale + ADR-0008 render-vs-content reconciliation); a note in CONTEXT.md §terminal (the grid is the OS-neutral render-replay layer) if warranted; no public CLI surface change.\n 82|- **Gate:** fresh-target clippy --workspace --all-targets + nextest (grid units + the alt-screen int) + traceable; the artifact bugs are inherently hard to unit-assert byte-exact — the int asserts *absence* of the known corruption markers in the initial batch.\n\n# .github/workflows/\n## ci.yml\n 438|      # as the spt build above).\n*439|      - name: Build mock-session (gated-step resume harness)\n*440|        run: cargo build -p mock-adapter --bin mock-session\n 441|      # The GATED SPT-SIDE STEP (REMOTE-TRUTH): the product-surface CLI rungs\n 442|      # over REAL `spt daemon run` processes — C-2 remote Wake-now revive,\n 443|      # E-1 cross-node from-stamp, B-3 remote-Severed reheal (destructive\n...\n 478|          SPT_TWO_HOST_NOTIFY_BIN: ${{ github.workspace }}\\.adapter-notify\\target\\debug\\notify-shell.exe\n*479|      - name: Build mock-session (gated-step resume harness)\n*480|        run: cargo build -p mock-adapter --bin mock-session\n 481|      - name: Two-host gated CLI step — role A (driver)\n 482|        run: cargo test -p spt --test twohost_cli -- --nocapture\n 483|        env:\n## release.yml\n 4|# runners, assemble the asset set the self-updater consumes (binaries,\n*5|# SHA256SUMS, manifest.schema.json, mock-adapter.zip, and the docs bundle\n 6|# spt-docs.tar.gz — ADR-0036 §4), and create a DRAFT release on the private\n 7|# channel BigscreenVR/spt-bs-releases (cross-repo via the RELEASES_TOKEN\n 8|# fine-grained PAT).\n...\n 108|        # The exact set REQ-REL-2 names: platform binaries + SHA256SUMS (what\n*109|        # the bootstrap flow verifies) + the manifest schema + the mock-adapter\n 110|        # source zip (the adapter quickstart's reference download).\n 111|        # The `spt-x86_64-*` glob picks up every platform binary the build matrix\n 112|        # produced (gnu, windows, musl, and any future registry row) — no\n...\n 118|          cp ../crates/spt-runtime/manifest.schema.json .\n*119|          # Zip rooted at the adapter dir so `unzip -d mock-adapter` yields\n*120|          # mock-adapter/manifest.toml exactly as the adapter quickstart says.\n*121|          (cd ../adapters/mock && zip -r \"$OLDPWD/mock-adapter.zip\" . \\\n 122|             -x 'target/*')\n 123|          ls -la\n 124|      - name: Ensure mdbook\n...\n 181|            dist/manifest.schema.json \\\n*182|            dist/mock-adapter.zip \\\n 183|            dist/spt-docs.tar.gz\n\n# adapters/\n\n## mock-gateway/\n### manifest.toml\n 6|# is human-backed: NO agent lifecycle (no tracked/ mind, no Psyche, no\n*7|# echo-commune/signoff). It reuses the generic `mock-session` helper as the fake\n 8|# binary — there is NO downstream (lecturn) code in spt-core, per the adapter\n 9|# boundary. The only Gateway-shaped artifact in-tree, the R-DOCS-2 mock pattern.\n 10|\n...\n 26|# The fake binary fires `api bind --type gateway` (REQ-EP-6 un-hardcode). The\n*27|# E2E rig rewrites this template to the freshly-built mock-session path.\n 28|[session.self]\n*29|command = \"mock-session --id {id} --session-id {session_id} --type gateway --mock-adapter mock-gateway\"\n 30|detach = true\n 31|keys = [\"id\", \"session_id\"]\n 32|\n...\n 34|session_id_source = \"post_spawn\"\n*35|parent_ancestor_name = \"mock-session\"\n\n## mock/\n### Cargo.toml\n 1|[package]\n*2|name = \"mock-adapter\"\n 3|version.workspace = true\n 4|edition = \"2021\"\n 5|publish = false\n...\n 8|[[bin]]\n*9|name = \"mock-session\"\n 10|path = \"src/main.rs\"\n 11|\n 12|[[bin]]\n### manifest.toml\n 3|# vendor config dir, no vendor hook names. The command templates spawn the\n*4|# trivial `mock-session` helper; the [hooks.*] table binds generic event names to\n 5|# the inbound `api` surface.\n 6|#\n 7|# This is the only adapter-shaped artifact in spt-core (PRD R-DOCS-2). A real\n...\n 39|\n*40|# Outbound session role: the opaque template ManifestRuntime spawns. `mock-session`\n 41|# stands in for a real harness binary; spt-core just runs it with keys filled.\n 42|[session.self]\n*43|command = \"mock-session --id {id} --session-id {session_id}\"\n 44|detach = true\n 45|keys = [\"id\", \"session_id\"]\n 46|\n...\n 51|[session.psyche_init]\n*52|command = \"mock-session --mode psyche --id {id} --session-id {session_id}\"\n 53|recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 54|detach = true\n 55|keys = [\"id\", \"session_id\"]\n...\n 57|[session.psyche_resume]\n*58|command = \"mock-session --mode psyche --id {id} --session-id {session_id}\"\n 59|recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 60|detach = true\n 61|keys = [\"id\", \"session_id\"]\n...\n 64|[session.echo_commune]\n*65|command = \"mock-session --mode echo --id {id}\"\n 66|recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 67|keys = [\"id\"]\n 68|\n...\n 70|[session.signoff]\n*71|command = \"mock-session --mode echo --id {id}\"\n 72|recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 73|keys = [\"id\"]\n 74|\n...\n 90|session_id_source = \"post_spawn\"\n*91|parent_ancestor_name = \"mock-session\"\n\n### src/\n#### main.rs\n*1|//! `mock-session` — the generic mock adapter's \"agent session\". A trivial,\n 2|//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,\n 3|//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's\n 4|//! `[session.self]` template, it does what a real session's boot hook would do:\n...\n 60|            let id = arg(&args, \"--id\").unwrap_or_else(|| {\n*61|                eprintln!(\"mock-session --mode dummy: --id required\");\n 62|                std::process::exit(2);\n 63|            });\n 64|            let session_id =\n*65|                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n 66|            let spt = arg(&args, \"--spt\")\n 67|                .or_else(|| std::env::var(\"SPT_BIN\").ok())\n 68|                .unwrap_or_else(|| \"spt\".to_string());\n*69|            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n 70|            let endpoint_type = arg(&args, \"--type\");\n 71|            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\n 72|                .and_then(|s| s.parse().ok())\n...\n 119|                if !matches!(bind, Ok(s) if s.success()) {\n*120|                    eprintln!(\"mock-session --mode {mode}: api bind failed: {bind:?}\");\n 121|                    std::process::exit(1);\n 122|                }\n 123|            }\n...\n 128|            loop {\n*129|                println!(\"DUMMY_HARNESS_TICK:{id}:{n}\");\n 130|                let _ = std::io::stdout().flush();\n 131|                n += 1;\n 132|                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));\n...\n 138|    let id = arg(&args, \"--id\").unwrap_or_else(|| {\n*139|        eprintln!(\"mock-session: --id required\");\n 140|        std::process::exit(2);\n 141|    });\n*142|    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n 143|    let spt = arg(&args, \"--spt\")\n 144|        .or_else(|| std::env::var(\"SPT_BIN\").ok())\n 145|        .unwrap_or_else(|| \"spt\".to_string());\n...\n 148|    let endpoint_type = arg(&args, \"--type\");\n*149|    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n 150|\n 151|    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\n 152|    let mut bind_args = vec![\n...\n 166|    if !matches!(bind, Ok(s) if s.success()) {\n*167|        eprintln!(\"mock-session: api bind failed: {bind:?}\");\n 168|        std::process::exit(1);\n 169|    }\n 170|\n...\n 184|    if !matches!(state, Ok(s) if s.success()) {\n*185|        eprintln!(\"mock-session: api state idle failed: {state:?}\");\n 186|        std::process::exit(1);\n 187|    }\n 188|\n#### shell.rs\n 14|//!\n*15|//! Like `mock-session`, the `spt` binary resolves from `--spt <path>`, else\n 16|//! `$SPT_BIN`, else PATH — a dumb helper driving the public surface.\n 17|\n 18|use std::process::Command;\n\n### tests/\n#### manifest_valid.rs\n 20|    let self_role = m.session.self_.as_ref().expect(\"[session.self]\");\n*21|    assert!(self_role.command.starts_with(\"mock-session\"));\n 22|    assert!(self_role.command.contains(\"{id}\"));\n 23|    // Inbound hooks bind generic events to the api surface.\n 24|    assert!(m.hooks[\"SessionStart\"].fires.contains(\"api seed\"));\n\n# crates/\n\n## spt-daemon/src/\n### harnesshost.rs\n 334|    fn prepare_fills_id_and_session_into_self_command() {\n*335|        let m = harness_manifest(\"mock-session --id {id} --session-id {session_id}\");\n 336|        let prepared = prepare_harness_spawn(\"doyle\", \"mock\", \"sess-abc\", &m, false, None, None).unwrap();\n 337|        assert_eq!(\n 338|            prepared.tokens,\n 339|            vec![\n*340|                \"mock-session\".to_string(),\n 341|                \"--id\".to_string(),\n 342|                \"doyle\".to_string(),\n 343|                \"--session-id\".to_string(),\n...\n 356|             min_spt_core_version = \\\"0\\\"\\n\\n[session.self]\\n\\\n*357|             command = 'mock-session --id {id} --remote {id}--{node}'\\nkeys = [\\\"id\\\", \\\"node\\\"]\\n\",\n 358|        )\n 359|        .unwrap();\n 360|        let prepared =\n...\n 364|            vec![\n*365|                \"mock-session\".to_string(),\n 366|                \"--id\".to_string(),\n 367|                \"hall-a\".to_string(),\n 368|                \"--remote\".to_string(),\n...\n 385|             min_spt_core_version = \\\"0\\\"\\n\\n[session.self]\\n\\\n*386|             command = 'mock-session --remote {node}'\\nkeys = [\\\"node\\\"]\\n\",\n 387|        )\n 388|        .unwrap();\n 389|        let err = prepare_harness_spawn(\"hall-a\", \"mock\", \"s\", &m, false, None, None)\n...\n 539|        // Unknown {placeholder} errs naming the key.\n*540|        let bad = harness_manifest(\"mock-session --id {id} --boom {not_a_key}\");\n 541|        assert!(prepare_harness_spawn(\"e\", \"mock\", \"s\", &bad, false, None, None)\n 542|            .unwrap_err()\n 543|            .contains(\"not_a_key\"));\n...\n 566|        let m = harness_manifest_with_resume(\n*567|            \"mock-session --id {id} --session-id {session_id}\",\n*568|            \"mock-session --id {id} --resume {session_id}\",\n 569|        );\n 570|\n 571|        // is_resume=true → the RESUME template (`--resume`), filled with the id.\n...\n 576|            vec![\n*577|                \"mock-session\".to_string(),\n 578|                \"--id\".to_string(),\n 579|                \"doyle\".to_string(),\n 580|                \"--resume\".to_string(),\n...\n 591|            vec![\n*592|                \"mock-session\".to_string(),\n 593|                \"--id\".to_string(),\n 594|                \"doyle\".to_string(),\n 595|                \"--session-id\".to_string(),\n...\n 614|    fn prepare_resume_falls_back_to_self_when_no_resume_role() {\n*615|        let no_resume = harness_manifest(\"mock-session --id {id} --session-id {session_id}\");\n 616|        assert!(\n 617|            no_resume.session.resume.is_none(),\n 618|            \"fixture declares no [session.resume]\"\n...\n 625|            vec![\n*626|                \"mock-session\".to_string(),\n 627|                \"--id\".to_string(),\n 628|                \"e\".to_string(),\n 629|                \"--session-id\".to_string(),\n\n## spt/\n### Cargo.toml\n 16|# `CARGO_BIN_EXE_translate_proof_fixture` to the integration test (the same\n*17|# cross-crate fixture-bin pattern as `mock-session`). The name MUST stay distinct\n 18|# from spt-daemon's `xlate_choreo_fixture` bin: an identical bin name collides on\n 19|# the shared `target/<profile>/` output filename and the two clobber each other —\n 20|# the daemon's `g3_content_free_ping` then resolves `CARGO_BIN_EXE_xlate_choreo_fixture`\n\n### tests/\n#### attach_wedge_e2e.rs\n 114|\n*115|/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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...\n 141|                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*142|                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 143|                        let _ = tx.send(true);\n 144|                        break;\n 145|                    }\n...\n 173|        .unwrap_or_default();\n*174|    let mock_session = sibling_bin(\"mock-session\");\n 175|    assert!(\n 176|        mock_session.exists(),\n 177|        \"the dummy-harness program must be built: {} \\\n*178|         (pre-build: cargo build -p mock-adapter --bin mock-session)\",\n 179|        mock_session.display()\n 180|    );\n 181|\n...\n 186|        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\n*187|         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"\n 188|    );\n 189|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n 190|    std::fs::create_dir_all(&src).unwrap();\n#### bind_cwd_project_e2e.rs\n 109|        .unwrap_or_default();\n*110|    let mock_session = sibling_bin(\"mock-session\");\n 111|    assert!(\n 112|        mock_session.exists(),\n 113|        \"the dummy-harness program must be built: {} \\\n*114|         (pre-build: cargo build -p mock-adapter --bin mock-session)\",\n 115|        mock_session.display()\n 116|    );\n 117|\n...\n 125|        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\n*126|         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"\n 127|    );\n 128|    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n 129|    std::fs::create_dir_all(&src).unwrap();\n\nShowing files 1-20 of 41. Use skip=20 for the next page, or narrow paths/pattern.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":69,"fileCount":20,"files":[".github/workflows/ci.yml",".github/workflows/release.yml","CONTEXT.md","Cargo.lock","M2b-PLAN.md","M5-D4-PLAN.md","M6-PLAN.md","M9-PLAN.md","V0.12.1-LIFECYCLE-JIT.md","V0.19.0-P6-SCREEN-GRID-DESIGN.md","adapters/mock-gateway/manifest.toml","adapters/mock/Cargo.toml","adapters/mock/manifest.toml","adapters/mock/src/main.rs","adapters/mock/src/shell.rs","adapters/mock/tests/manifest_valid.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt/Cargo.toml","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/bind_cwd_project_e2e.rs"],"fileMatches":[{"path":".github/workflows/ci.yml","count":4},{"path":".github/workflows/release.yml","count":6},{"path":"CONTEXT.md","count":1},{"path":"Cargo.lock","count":1},{"path":"M2b-PLAN.md","count":1},{"path":"M5-D4-PLAN.md","count":1},{"path":"M6-PLAN.md","count":4},{"path":"M9-PLAN.md","count":1},{"path":"V0.12.1-LIFECYCLE-JIT.md","count":1},{"path":"V0.19.0-P6-SCREEN-GRID-DESIGN.md","count":1},{"path":"adapters/mock-gateway/manifest.toml","count":4},{"path":"adapters/mock/Cargo.toml","count":2},{"path":"adapters/mock/manifest.toml","count":8},{"path":"adapters/mock/src/main.rs","count":11},{"path":"adapters/mock/src/shell.rs","count":1},{"path":"adapters/mock/tests/manifest_valid.rs","count":1},{"path":"crates/spt-daemon/src/harnesshost.rs","count":12},{"path":"crates/spt/Cargo.toml","count":1},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":5},{"path":"crates/spt/tests/bind_cwd_project_e2e.rs","count":3}],"truncated":true,"fileLimitReached":20,"displayContent":"# CONTEXT.md\n 863│**repo topology (The Forkening, ADR-0036)**:\n*864│Development home is **`BigscreenVR/spt-bs-core`** (private); the publish target is **`BigscreenVR/spt-bs-releases`** (private) — GitHub Releases carry the binaries, the docs bundle, and the signed update-set, reached only through an org-authenticated `gh`. **`SaberMage/spt-core` and `SaberMage/spt-releases` are retained as passive mirrors** (private; the merging agent pushes every main merge + tags; Actions disabled) — full history, may diverge from the dev home only if a genuine second contributor e...\n 865│_Avoid_: treating a releases repo as a second source repo; authoring doc truth there; publishing anything to the mirrors first.\n 866│\n 867│**issue / feature tracking**:\n\n# Cargo.lock\n 2341│[[package]]\n*2342│name = \"mock-adapter\"\n 2343│version = \"0.33.0\"\n 2344│dependencies = [\n 2345│ \"spt-runtime\",\n\n# M2b-PLAN.md\n 61│stale-signoff sweep); the **resume-session seam** (fresh-with-preload +\n*62│continue-existing); the `api listen` wiring that hosts all of it; mock-adapter\n 63│lifecycle templates + a LiveAgent E2E.\n 64│\n 65│**Out:**\n\n# M5-D4-PLAN.md\n 201│  slice over an in-flight run.\n*202│- `cargo build -p mock-adapter` before local `shell_e2e` iteration (no dep edge —\n 203│  stale-binary trap). Flags before variadic positionals in tests/scripts (clap trap).\n 204│- Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.\n\n# M6-PLAN.md\n  43│5. **Docs scope = Tier 1** (DOCS-STRATEGY §v0.1): mental model · human quickstart ·\n *44│   adapter quickstart (via `mock-adapter`) · **complete harness-contract vertical**\n  45│   (public manifest reference + full `spt api` reference + install-on-demand\n  46│   bootstrap pattern) · shell getting-started (`spt-shell-notify` live example) ·\n  47│   generated CLI reference · **manifest JSON Schema (new code: schemars)** · root\n    │...\n  65│   `SHA256SUMS`, **`SignedRelease` metadata in the exact format `spt update`\n *66│   consumes**, `manifest.schema.json`, `mock-adapter` source zip.\n  67│10. **Signing (ADR-0015):** two Ed25519 keypairs — active primary + never-used\n  68│    offline recovery; **both pubkeys embedded** in the binary's trusted set; seeds\n  69│    backed up off-machine; **manual local signing** (no CI secrets).\n *70│11. **License split:** MIT for install scripts / `mock-adapter` / docs-site\n  71│    content; binary under a short proprietary freeware license with an explicit\n  72│    **royalty-free adapter/shell development clause**.\n  73│12. **Wire-proto version** stays its own counter (R-ARCH-3) — never conflated with\n    │...\n 123│  mental model/overview (diagrams); human quickstart (install → two agents\n*124│  message, real values); adapter quickstart (walked via `mock-adapter`);\n 125│  harness-contract vertical complete (public manifest reference rewritten from\n 126│  `docs/MANIFEST.md` — public-safe, no sister-project references; full `spt api`\n 127│  reference; install-on-demand bootstrap pattern + per-OS snippet); shell\n\n# M9-PLAN.md\n 129│subscribes to digests, and is the T4 gate's user-backed origin. In-tree mock\n*130│gateway fixture (manifest + fake binary, the R-DOCS-2 mock-adapter pattern —\n 131│no lecturn code in spt-core, per the adapter boundary). Evidence:\n 132│`REQ-EP-<n>` `[doc, impl, unit]`.\n 133│\n\n# V0.12.1-LIFECYCLE-JIT.md\n 41│  endpoint-run session reads the wrong/empty log. GATE (dummy harness): rc attach to a LIVE endpoint-run harness RECEIVES\n*42│  its DUMMY_HARNESS_TICK within a bounded window.\n 43│\n 44│- **L1 — design GATE-PASS @5ae68f8 (doc+impl+unit landed; int = OPERATOR MANUAL ACCEPTANCE, awaiting operator real-env test).**\n 45│  Landed: CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths (daemon.rs `detached_no_inherit` + deelevate.rs token spawn),\n\n# V0.19.0-P6-SCREEN-GRID-DESIGN.md\n 78│- **Units:** vte→grid application (print/CSI/SGR/cursor/alt-screen enter-exit → cell/cursor state); `render_repaint` produces a clean paint for (a) a main-screen state, (b) an alt-screen TUI state, (c) after a resize; repaint pseudo-seq monotonicity; grid single-writer / no-lock-across-fanout.\n*79│- **Int:** a broker session driven with a scripted alt-screen byte sequence (enter alt-screen, paint, scroll, partial repaint) → a fresh attach receives a CLEAN current-screen repaint (assert no raw alt-screen-enter / no torn-chunk debris in the initial batch), then live frames continue in-order. Reuse the dummy-harness/mock-adapter fixture pattern.\n 80│- **[twohost]:** NOT needed — this is single-host attach-render (the grid is server-side, the wire is unchanged; #4 already owns the cross-node int). A single-host int suffices.\n 81│- **Docs:** ADR-0031 (companion: vte dep rationale + ADR-0008 render-vs-content reconciliation); a note in CONTEXT.md §terminal (the grid is the OS-neutral render-replay layer) if warranted; no public CLI surface change.\n 82│- **Gate:** fresh-target clippy --workspace --all-targets + nextest (grid units + the alt-screen int) + traceable; the artifact bugs are inherently hard to unit-assert byte-exact — the int asserts *absence* of the known corruption markers in the initial batch.\n\n# .github/workflows/\n## ci.yml\n 438│      # as the spt build above).\n*439│      - name: Build mock-session (gated-step resume harness)\n*440│        run: cargo build -p mock-adapter --bin mock-session\n 441│      # The GATED SPT-SIDE STEP (REMOTE-TRUTH): the product-surface CLI rungs\n 442│      # over REAL `spt daemon run` processes — C-2 remote Wake-now revive,\n 443│      # E-1 cross-node from-stamp, B-3 remote-Severed reheal (destructive\n    │...\n 478│          SPT_TWO_HOST_NOTIFY_BIN: ${{ github.workspace }}\\.adapter-notify\\target\\debug\\notify-shell.exe\n*479│      - name: Build mock-session (gated-step resume harness)\n*480│        run: cargo build -p mock-adapter --bin mock-session\n 481│      - name: Two-host gated CLI step — role A (driver)\n 482│        run: cargo test -p spt --test twohost_cli -- --nocapture\n 483│        env:\n## release.yml\n   4│# runners, assemble the asset set the self-updater consumes (binaries,\n  *5│# SHA256SUMS, manifest.schema.json, mock-adapter.zip, and the docs bundle\n   6│# spt-docs.tar.gz — ADR-0036 §4), and create a DRAFT release on the private\n   7│# channel BigscreenVR/spt-bs-releases (cross-repo via the RELEASES_TOKEN\n   8│# fine-grained PAT).\n    │...\n 108│        # The exact set REQ-REL-2 names: platform binaries + SHA256SUMS (what\n*109│        # the bootstrap flow verifies) + the manifest schema + the mock-adapter\n 110│        # source zip (the adapter quickstart's reference download).\n 111│        # The `spt-x86_64-*` glob picks up every platform binary the build matrix\n 112│        # produced (gnu, windows, musl, and any future registry row) — no\n    │...\n 118│          cp ../crates/spt-runtime/manifest.schema.json .\n*119│          # Zip rooted at the adapter dir so `unzip -d mock-adapter` yields\n*120│          # mock-adapter/manifest.toml exactly as the adapter quickstart says.\n*121│          (cd ../adapters/mock && zip -r \"$OLDPWD/mock-adapter.zip\" . \\\n 122│             -x 'target/*')\n 123│          ls -la\n 124│      - name: Ensure mdbook\n    │...\n 181│            dist/manifest.schema.json \\\n*182│            dist/mock-adapter.zip \\\n 183│            dist/spt-docs.tar.gz\n\n# adapters/\n\n## mock-gateway/\n### manifest.toml\n  6│# is human-backed: NO agent lifecycle (no tracked/ mind, no Psyche, no\n *7│# echo-commune/signoff). It reuses the generic `mock-session` helper as the fake\n  8│# binary — there is NO downstream (lecturn) code in spt-core, per the adapter\n  9│# boundary. The only Gateway-shaped artifact in-tree, the R-DOCS-2 mock pattern.\n 10│\n   │...\n 26│# The fake binary fires `api bind --type gateway` (REQ-EP-6 un-hardcode). The\n*27│# E2E rig rewrites this template to the freshly-built mock-session path.\n 28│[session.self]\n*29│command = \"mock-session --id {id} --session-id {session_id} --type gateway --mock-adapter mock-gateway\"\n 30│detach = true\n 31│keys = [\"id\", \"session_id\"]\n 32│\n   │...\n 34│session_id_source = \"post_spawn\"\n*35│parent_ancestor_name = \"mock-session\"\n\n## mock/\n### Cargo.toml\n  1│[package]\n *2│name = \"mock-adapter\"\n  3│version.workspace = true\n  4│edition = \"2021\"\n  5│publish = false\n   │...\n  8│[[bin]]\n *9│name = \"mock-session\"\n 10│path = \"src/main.rs\"\n 11│\n 12│[[bin]]\n### manifest.toml\n  3│# vendor config dir, no vendor hook names. The command templates spawn the\n *4│# trivial `mock-session` helper; the [hooks.*] table binds generic event names to\n  5│# the inbound `api` surface.\n  6│#\n  7│# This is the only adapter-shaped artifact in spt-core (PRD R-DOCS-2). A real\n   │...\n 39│\n*40│# Outbound session role: the opaque template ManifestRuntime spawns. `mock-session`\n 41│# stands in for a real harness binary; spt-core just runs it with keys filled.\n 42│[session.self]\n*43│command = \"mock-session --id {id} --session-id {session_id}\"\n 44│detach = true\n 45│keys = [\"id\", \"session_id\"]\n 46│\n   │...\n 51│[session.psyche_init]\n*52│command = \"mock-session --mode psyche --id {id} --session-id {session_id}\"\n 53│recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 54│detach = true\n 55│keys = [\"id\", \"session_id\"]\n   │...\n 57│[session.psyche_resume]\n*58│command = \"mock-session --mode psyche --id {id} --session-id {session_id}\"\n 59│recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 60│detach = true\n 61│keys = [\"id\", \"session_id\"]\n   │...\n 64│[session.echo_commune]\n*65│command = \"mock-session --mode echo --id {id}\"\n 66│recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 67│keys = [\"id\"]\n 68│\n   │...\n 70│[session.signoff]\n*71│command = \"mock-session --mode echo --id {id}\"\n 72│recursion_guard_env = \"MOCK_ECHO_COMMUNE\"\n 73│keys = [\"id\"]\n 74│\n   │...\n 90│session_id_source = \"post_spawn\"\n*91│parent_ancestor_name = \"mock-session\"\n\n### src/\n#### main.rs\n  *1│//! `mock-session` — the generic mock adapter's \"agent session\". A trivial,\n   2│//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,\n   3│//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's\n   4│//! `[session.self]` template, it does what a real session's boot hook would do:\n    │...\n  60│            let id = arg(&args, \"--id\").unwrap_or_else(|| {\n *61│                eprintln!(\"mock-session --mode dummy: --id required\");\n  62│                std::process::exit(2);\n  63│            });\n  64│            let session_id =\n *65│                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n  66│            let spt = arg(&args, \"--spt\")\n  67│                .or_else(|| std::env::var(\"SPT_BIN\").ok())\n  68│                .unwrap_or_else(|| \"spt\".to_string());\n *69│            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n  70│            let endpoint_type = arg(&args, \"--type\");\n  71│            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\n  72│                .and_then(|s| s.parse().ok())\n    │...\n 119│                if !matches!(bind, Ok(s) if s.success()) {\n*120│                    eprintln!(\"mock-session --mode {mode}: api bind failed: {bind:?}\");\n 121│                    std::process::exit(1);\n 122│                }\n 123│            }\n    │...\n 128│            loop {\n*129│                println!(\"DUMMY_HARNESS_TICK:{id}:{n}\");\n 130│                let _ = std::io::stdout().flush();\n 131│                n += 1;\n 132│                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));\n    │...\n 138│    let id = arg(&args, \"--id\").unwrap_or_else(|| {\n*139│        eprintln!(\"mock-session: --id required\");\n 140│        std::process::exit(2);\n 141│    });\n*142│    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n 143│    let spt = arg(&args, \"--spt\")\n 144│        .or_else(|| std::env::var(\"SPT_BIN\").ok())\n 145│        .unwrap_or_else(|| \"spt\".to_string());\n    │...\n 148│    let endpoint_type = arg(&args, \"--type\");\n*149│    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n 150│\n 151│    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\n 152│    let mut bind_args = vec![\n    │...\n 166│    if !matches!(bind, Ok(s) if s.success()) {\n*167│        eprintln!(\"mock-session: api bind failed: {bind:?}\");\n 168│        std::process::exit(1);\n 169│    }\n 170│\n    │...\n 184│    if !matches!(state, Ok(s) if s.success()) {\n*185│        eprintln!(\"mock-session: api state idle failed: {state:?}\");\n 186│        std::process::exit(1);\n 187│    }\n 188│\n#### shell.rs\n 14│//!\n*15│//! Like `mock-session`, the `spt` binary resolves from `--spt <path>`, else\n 16│//! `$SPT_BIN`, else PATH — a dumb helper driving the public surface.\n 17│\n 18│use std::process::Command;\n\n### tests/\n#### manifest_valid.rs\n 20│    let self_role = m.session.self_.as_ref().expect(\"[session.self]\");\n*21│    assert!(self_role.command.starts_with(\"mock-session\"));\n 22│    assert!(self_role.command.contains(\"{id}\"));\n 23│    // Inbound hooks bind generic events to the api surface.\n 24│    assert!(m.hooks[\"SessionStart\"].fires.contains(\"api seed\"));\n\n# crates/\n\n## spt-daemon/src/\n### harnesshost.rs\n 334│    fn prepare_fills_id_and_session_into_self_command() {\n*335│        let m = harness_manifest(\"mock-session --id {id} --session-id {session_id}\");\n 336│        let prepared = prepare_harness_spawn(\"doyle\", \"mock\", \"sess-abc\", &m, false, None, None).unwrap();\n 337│        assert_eq!(\n 338│            prepared.tokens,\n 339│            vec![\n*340│                \"mock-session\".to_string(),\n 341│                \"--id\".to_string(),\n 342│                \"doyle\".to_string(),\n 343│                \"--session-id\".to_string(),\n    │...\n 356│             min_spt_core_version = \\\"0\\\"\\n\\n[session.self]\\n\\\n*357│             command = 'mock-session --id {id} --remote {id}--{node}'\\nkeys = [\\\"id\\\", \\\"node\\\"]\\n\",\n 358│        )\n 359│        .unwrap();\n 360│        let prepared =\n    │...\n 364│            vec![\n*365│                \"mock-session\".to_string(),\n 366│                \"--id\".to_string(),\n 367│                \"hall-a\".to_string(),\n 368│                \"--remote\".to_string(),\n    │...\n 385│             min_spt_core_version = \\\"0\\\"\\n\\n[session.self]\\n\\\n*386│             command = 'mock-session --remote {node}'\\nkeys = [\\\"node\\\"]\\n\",\n 387│        )\n 388│        .unwrap();\n 389│        let err = prepare_harness_spawn(\"hall-a\", \"mock\", \"s\", &m, false, None, None)\n    │...\n 539│        // Unknown {placeholder} errs naming the key.\n*540│        let bad = harness_manifest(\"mock-session --id {id} --boom {not_a_key}\");\n 541│        assert!(prepare_harness_spawn(\"e\", \"mock\", \"s\", &bad, false, None, None)\n 542│            .unwrap_err()\n 543│            .contains(\"not_a_key\"));\n    │...\n 566│        let m = harness_manifest_with_resume(\n*567│            \"mock-session --id {id} --session-id {session_id}\",\n*568│            \"mock-session --id {id} --resume {session_id}\",\n 569│        );\n 570│\n 571│        // is_resume=true → the RESUME template (`--resume`), filled with the id.\n    │...\n 576│            vec![\n*577│                \"mock-session\".to_string(),\n 578│                \"--id\".to_string(),\n 579│                \"doyle\".to_string(),\n 580│                \"--resume\".to_string(),\n    │...\n 591│            vec![\n*592│                \"mock-session\".to_string(),\n 593│                \"--id\".to_string(),\n 594│                \"doyle\".to_string(),\n 595│                \"--session-id\".to_string(),\n    │...\n 614│    fn prepare_resume_falls_back_to_self_when_no_resume_role() {\n*615│        let no_resume = harness_manifest(\"mock-session --id {id} --session-id {session_id}\");\n 616│        assert!(\n 617│            no_resume.session.resume.is_none(),\n 618│            \"fixture declares no [session.resume]\"\n    │...\n 625│            vec![\n*626│                \"mock-session\".to_string(),\n 627│                \"--id\".to_string(),\n 628│                \"e\".to_string(),\n 629│                \"--session-id\".to_string(),\n\n## spt/\n### Cargo.toml\n 16│# `CARGO_BIN_EXE_translate_proof_fixture` to the integration test (the same\n*17│# cross-crate fixture-bin pattern as `mock-session`). The name MUST stay distinct\n 18│# from spt-daemon's `xlate_choreo_fixture` bin: an identical bin name collides on\n 19│# the shared `target/<profile>/` output filename and the two clobber each other —\n 20│# the daemon's `g3_content_free_ping` then resolves `CARGO_BIN_EXE_xlate_choreo_fixture`\n\n### tests/\n#### attach_wedge_e2e.rs\n 114│\n*115│/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\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    │...\n 141│                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n*142│                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n 143│                        let _ = tx.send(true);\n 144│                        break;\n 145│                    }\n    │...\n 173│        .unwrap_or_default();\n*174│    let mock_session = sibling_bin(\"mock-session\");\n 175│    assert!(\n 176│        mock_session.exists(),\n 177│        \"the dummy-harness program must be built: {} \\\n*178│         (pre-build: cargo build -p mock-adapter --bin mock-session)\",\n 179│        mock_session.display()\n 180│    );\n 181│\n    │...\n 186│        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\n*187│         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"\n 188│    );\n 189│    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n 190│    std::fs::create_dir_all(&src).unwrap();\n#### bind_cwd_project_e2e.rs\n 109│        .unwrap_or_default();\n*110│    let mock_session = sibling_bin(\"mock-session\");\n 111│    assert!(\n 112│        mock_session.exists(),\n 113│        \"the dummy-harness program must be built: {} \\\n*114│         (pre-build: cargo build -p mock-adapter --bin mock-session)\",\n 115│        mock_session.display()\n 116│    );\n 117│\n    │...\n 125│        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\n*126│         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"\n 127│    );\n 128│    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n 129│    std::fs::create_dir_all(&src).unwrap();","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195828191}}
{"type":"message","id":"76993701","parentId":"6e4f8382","timestamp":"2026-07-16T09:57:23.517Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning specific cargo test commands**\n\n**Inspecting mock-session stdin handling**"},{"type":"toolCall","id":"call_q5Ezx5awDfCMXgvCPIrGnmJr|fc_0b42584a8ab0545d016a58ab0379248199a69e3736d6dfe475","name":"read","arguments":{"i":"Reading dummy harness behavior","path":"adapters/mock/src/main.rs","selector":"1-150"}},{"type":"toolCall","id":"call_FLFlXuEqBszMtT42HiYPSZrG|fc_0b42584a8ab0545d016a58ab0379308199a184b99405f6b9e5","name":"read","arguments":{"i":"Reading rc data path tests","path":"crates/spt/src/rc.rs","selector":"1-180"}},{"type":"toolCall","id":"call_EWnhhR0cwB75NlJnzupGNEE8|fc_0b42584a8ab0545d016a58ab03793c8199a26a86cb15e21154","name":"grep","arguments":{"i":"Finding rc stdin pump symbols","pattern":"spawn_stdin|stdin.*reader|send_attach_input|stdin_rx|read\\(&mut|write_all|detach","path":"crates/spt/src/rc.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_HygIkHxgA3kUrpDw3xXwwP85|fc_0b42584a8ab0545d016a58ab037940819995c5c665bf14799c","name":"read","arguments":{"i":"Reading physical conn regression gate","path":"crates/spt-daemon/tests/brain_decouple.rs","selector":"430-835"}},{"type":"toolCall","id":"call_umRpPd6vL9pVIhc2XQQrs1U2|fc_0b42584a8ab0545d016a58ab0379488199bce11a13be14ba59","name":"read","arguments":{"i":"Reading blackhole lifecycle gate","path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","selector":"1-330,560-650"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14597,"output":438,"cacheRead":202240,"cacheWrite":0,"totalTokens":217275,"reasoningTokens":196,"cost":{"input":0.07298500000000001,"output":0.01314,"cacheRead":0.10112,"cacheWrite":0,"total":0.187245}},"stopReason":"toolUse","timestamp":1784195828239,"responseId":"resp_0b42584a8ab0545d016a58aaf48cf88199b3deb0cd97d57fe6","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58aafb87f48199b6bc4f670f42e655","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsD0l1I17CXEwgz_UwIiL8AszUcQA3qhUMbNgY-Ir4IDq7NKrxek9Aq0ogrgQiWp6tureASMjuo1ie0TZZJC7ACczo5uGJW9tc6wek8ow453Wby4cBx-gzTYi1Qb1ujuqUqgo16aqLoeOOKKWT4YRz3QWFbjLTSXiqebD6UNL7Q7pgVrqbzcQEd4NtGDNcuibZDQak7udjWDKSdFD4Yk6cQ3-GLg4Y2R6edBg1bSooZHciRfMEDFjCfevDwHTaVTw-2GXzzHhcLpIDidDXwukR-FvzBNgZNhWx-L9HdpvHtQQ6KnZlydJoGqZLODRlRj9-5rbAfkGrQ0EkXIcNg8T8T10uY_wLBVsKwtqHVPr3-8-3YkgB5P0taJgoFwhYgRcvr-ImjLMImgoOqCfXcVW7Hwnw1p28ubizxBoBpzxFgwGRBARDxdB05-Mjk0UMRTRnfcI8ov0NPFVB6y10GiAJcZP3ZKfbPnBvsiChgz0OyK0qp1jlhMj_uPSDI3TBo_a5hYLc7wff8mb8J8P6IxFPIigSDbHjFRi796s22OfNo-S6EAWPBiZT-el9EUNbgs38ozgmMI8m7Zj0mGXH6AlB4j60IKY-DSpTG-wR17MrNL6dWPc7ACzDeqUjBBGzVEn4gfjcqTRrieh2aTjyiXmIv5uFyZEtEJrHjVd4AWOUHJnlzW77yTOVwf8RYgqKBRpn-iU_SF9kfcNyRGra1hquTAND6j9KIMq8TIgCJX9SyExMDJZ1jRv6va0qwTaurshTvQO85UE4MiDG6cg7eSevjZhcEZAXjVI997kbYEXAgT9ccgg7PWf__Yern7qN87-bgQQZUhtSSJug1zNYPDTL-SaksZb8nRhwHKkv2da6ppV0NZ8jn9IDwpu45mX8H5yVx_WBqWNeg0iSvZAs-t-pkwd3TzzTmyPZwkPLmuwvrcaERDIQr1nFFq0NFQ0AwdPahNarvq5X7uZrC_A1UrpOMypFJsa8XQO-7UlX640XzD_TUUSPApFJA_XHhz3Y9lAyL31z33grmP-PNQA-Ms3C9pjwXweZWMrPQx8Js7k-ol46fHbbSmZOAUdrgQ9yiCTZJAj9f5wgWZJDvVaIVPLeLQsOjIpqLqCs2eEx-MWAnTj9gnBuVGRsGSAVSYGc8CiS8kOieSjSWr6ao9NiPooHrHkQR_dBVQ0Kcq8zhbkp9RRAPg_A8b8lXk3Cv_zCcGpiTEPala_iqHV363Wz8MxEzwPTjh4nkb13xvYW8EfN9Ax3iF2jORWEgUxdwhlPP0dYgHalLDlhy829VfxG90FNSUpAQGo-42yPb84Nzib2W5vMqChYUfOpNjsX9CWJr0bUr9cxspOLSM-vMOde-roITgz3uek5VU_dZapUyVLMwlyEQESD5Io3ytSAcPKl7JzmkRxmuHSvGVXC2EGJYQW_o0j3m84Hw3qAGxhlP-tPuGvqZRUzwG1R_MwBfIiI0HRdao3TKaX-wzg6VOswzR9QFPnlbQe8AzEOhvQUQrxlY1YXoSCXYPfz9FJ04tR6QON18YMVsjljYNhbYC4ZtCGWEK80Bef04NOJXxrlayfeYLdClffCyuDo0JiciWV92PBWdYCjMIOrTjykbEbBvTKpXM1bmoz8Gsup91lVTUU8vtjOwM3yKfZzEpo2_H3nRW8a9rwJpXeqlmaO4ikiUc_6nNDTmEzLzgTXKWMAHaYyIEqP-JH-Q0nLwZAsaFQ9uMyU6fpM9C2nkf1mw7EnuWsfGLoATfCmPSlvsWXbz5uP6S9b-qlLuTMolK7Wum5r7pvfcvry6f6oCGWhRT8uyeTFLVpu0elqZurYfCxcIwEcBUT1mV5ocEH0aEOxct45Ya1TwxkBSZUConcHAQfggXpcicDk_-XEdWV2o3vjMClOj_ZkPw4-QVHRSEiLQa5GxGgSM9nhiUkbid-iDRPNSXmj_2WSc1R5wReN16H2P2CoAs9FTRr4pibfQQ3mNkrRF_eL-BO3Sbx0s-e_ipnU_UyBsL65vUuGB6SeZ4wZBlwIDJ5M=","summary":[{"type":"summary_text","text":"**Planning specific cargo test commands**"},{"type":"summary_text","text":"**Inspecting mock-session stdin handling**"}]},{"id":"fc_0b42584a8ab0545d016a58ab0379248199a69e3736d6dfe475","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dummy harness behavior\",\"path\":\"adapters/mock/src/main.rs\",\"selector\":\"1-150\"}","call_id":"call_q5Ezx5awDfCMXgvCPIrGnmJr","name":"read"},{"id":"fc_0b42584a8ab0545d016a58ab0379308199a184b99405f6b9e5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc data path tests\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1-180\"}","call_id":"call_FLFlXuEqBszMtT42HiYPSZrG","name":"read"},{"id":"fc_0b42584a8ab0545d016a58ab03793c8199a26a86cb15e21154","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding rc stdin pump symbols\",\"pattern\":\"spawn_stdin|stdin.*reader|send_attach_input|stdin_rx|read\\\\(&mut|write_all|detach\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_EWnhhR0cwB75NlJnzupGNEE8","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58ab037940819995c5c665bf14799c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading physical conn regression gate\",\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"selector\":\"430-835\"}","call_id":"call_HygIkHxgA3kUrpDw3xXwwP85","name":"read"},{"id":"fc_0b42584a8ab0545d016a58ab0379488199bce11a13be14ba59","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading blackhole lifecycle gate\",\"path\":\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\",\"selector\":\"1-330,560-650\"}","call_id":"call_umRpPd6vL9pVIhc2XQQrs1U2","name":"read"}]},"duration":15277.78509999998,"ttft":7276.005900000222,"contextSnapshot":{"promptTokens":216837,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_q5Ezx5awDfCMXgvCPIrGnmJr|fc_0b42584a8ab0545d016a58ab0379248199a69e3736d6dfe475","toolName":"read","startedAt":"2026-07-16T09:57:23.518Z","args":{"path":"adapters/mock/src/main.rs"},"intent":"Reading dummy harness behavior"},"id":"bfbe713e","parentId":"76993701","timestamp":"2026-07-16T09:57:23.518Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FLFlXuEqBszMtT42HiYPSZrG|fc_0b42584a8ab0545d016a58ab0379308199a184b99405f6b9e5","toolName":"read","startedAt":"2026-07-16T09:57:23.518Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc data path tests"},"id":"bcff5fc8","parentId":"bfbe713e","timestamp":"2026-07-16T09:57:23.518Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_EWnhhR0cwB75NlJnzupGNEE8|fc_0b42584a8ab0545d016a58ab03793c8199a26a86cb15e21154","toolName":"grep","startedAt":"2026-07-16T09:57:23.518Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Finding rc stdin pump symbols"},"id":"d4303f6f","parentId":"bcff5fc8","timestamp":"2026-07-16T09:57:23.518Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HygIkHxgA3kUrpDw3xXwwP85|fc_0b42584a8ab0545d016a58ab037940819995c5c665bf14799c","toolName":"read","startedAt":"2026-07-16T09:57:23.518Z","args":{"path":"crates/spt-daemon/tests/brain_decouple.rs"},"intent":"Reading physical conn regression gate"},"id":"48c6a50b","parentId":"d4303f6f","timestamp":"2026-07-16T09:57:23.518Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_umRpPd6vL9pVIhc2XQQrs1U2|fc_0b42584a8ab0545d016a58ab0379488199bce11a13be14ba59","toolName":"read","startedAt":"2026-07-16T09:57:23.518Z","args":{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs"},"intent":"Reading blackhole lifecycle gate"},"id":"fed2794b","parentId":"48c6a50b","timestamp":"2026-07-16T09:57:23.518Z"}
{"type":"message","id":"b7865475","parentId":"fed2794b","timestamp":"2026-07-16T09:57:23.525Z","message":{"role":"toolResult","toolCallId":"call_umRpPd6vL9pVIhc2XQQrs1U2|fc_0b42584a8ab0545d016a58ab0379488199bce11a13be14ba59","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\nuse std::sync::atomic::{AtomicU32, AtomicU64, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, encode_bytes, InputReq, OutputEvent, SessionsReply, SpawnReq, Spawned,\n    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::AttachIntent;\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-blackhole-lc-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\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\nfn kill_pid(pid: u32) {\n    let _ = std::process::Command::new(\"taskkill\")\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n        .output();\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 connect(name: &str) -> Stream {\n    for _ in 0..200 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n/// session so its viewer has a continuous frame stream to keep ticking on\n/// throughout the black-hole incident (invariant 1).\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n        program: \"cmd\".to_string(),\n        args: vec![\n            \"/c\".to_string(),\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\n        ],\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/// A real PTY child (PowerShell) that emits `SEED`, parks on a first `Read-Host`,\n/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\nfn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n        program: \"powershell\".to_string(),\n        args: vec![\n            \"-NoProfile\".to_string(),\n            \"-Command\".to_string(),\n            \"Write-Output SEED; $null = Read-Host; \\\n             foreach ($i in 1..1400) { Write-Output ('B' * 1023) }; $null = Read-Host\"\n                .to_string(),\n        ],\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/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\nfn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n    let mut c = connect(name);\n    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n    let sid = loop {\n        let f = read_frame(&mut c).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    (c, sid)\n}\n\n/// Query the broker's stall-evict tally. Bounded thread + channel (a wedge\n/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n    let name = name.to_string();\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n                    let _ = tx.send(r.count);\n                    return;\n                }\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = tx.send(0);\n                    return;\n                }\n            }\n        }\n    });\n    rx.recv_timeout(deadline).unwrap_or(0)\n}\n\n/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\nfn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n    let name = name.to_string();\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(&mut c, KIND_SESSIONS, serde_json::Value::Null);\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_SESSIONS_REPLY => {\n                    let r: SessionsReply = serde_json::from_value(f.payload).unwrap();\n                    let _ = tx.send(r);\n                    return;\n                }\n                Ok(_) => continue,\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…\n    });\n…\n    thread::spawn(move || {\n…\n        while Instant::now() < deadline && (outcome.is_none() || !got_output) {\n            match read_frame(&mut c) {\n…\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    if !got_output {\n                        let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n                        first_seq = Some(ev.seq);\n                        got_output = true;\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        let _ = fv_tx.send((outcome, got_output, first_seq));\n    });\n    let (fv_outcome, fv_got_output, fv_first_seq) = fv_rx\n        .recv_timeout(Duration::from_secs(18))\n        .unwrap_or((None, false, None));\n\n    // ── INVARIANT 1 final sample: the unrelated viewer's frame count must have\n    //    ADVANCED across the whole incident window (baseline → now). It kept\n    //    receiving output while B's controller conn was black-holed + retired. ──\n    let u_final = u_frames.load(Ordering::Relaxed);\n\n    // ── Teardown BEFORE asserting (so a failing assert leaves no orphan child):\n    //    kill both PTY children, stop the broker, drop the black-holed Stream. ──\n    if let Some(pid) = broker.session_pid(sid_b) {\n        kill_pid(pid);\n    }\n    if let Some(pid) = broker.session_pid(sid_u) {\n        kill_pid(pid);\n    }\n    broker.stop();\n    drop(r_stream);\n\n    eprintln!(\n        \"=== BLACKHOLE-LC HARNESS: saw_seed={saw_seed} seed_high={seed_high} \\\n         max_drained_seq={max_drained_seq} burst_advanced={burst_advanced} \\\n         u_baseline={u_baseline} u_at_release={u_at_release} u_final={u_final} \\\n         stall_evicts_seen={stall_evicts_seen} \\\n         handler_released={handler_released} old_client_write_is_err={old_client_write_is_err:?} \\\n         control_plane_ok={control_plane_ok} control_plane_elapsed={control_plane_elapsed:?} \\\n         resume_seq={resume_seq} fv_outcome={fv_outcome:?} fv_got_output={fv_got_output} \\\n         fv_first_seq={fv_first_seq:?} ===\"\n    );\n\n    // ── HARNESS preconditions (test-environment, distinct from the product\n    //    invariants): the black-hole must have actually been established, else the\n    //    five-invariant observation is vacuous. ──\n    assert!(\n        old_client_write_is_err.is_some(),\n        \"HARNESS: the black-holed controller never handed its stream back — the \\\n         PowerShell child likely never produced SEED (saw_seed={saw_seed}). This is a \\\n         test-environment failure, not a product regression.\"\n    );\n    assert!(\n        burst_advanced,\n        \"HARNESS: the victim PTY/ring high-water must advance ≥64 frames past the \\\n         burst trigger (seed_high={seed_high}) — the finite burst must actually flow.\"\n    );\n    assert!(\n        max_drained_seq > 0,\n        \"HARNESS: the controller must have drained a real seq>0 frame before \\\n         black-holing (load-gated repro); got max_drained_seq={max_drained_seq}\"\n    );\n    assert!(\n        (1..=2).contains(&stall_evicts_seen),\n        \"HARNESS: the one wedged controller's release must be tallied once (logical \\\n         reap OR physical poison) or twice (both raced — poison-beats-evict, \\\n         field-accepted v0.30.6); got {stall_evicts_seen}\"\n    );\n\n    // ── INVARIANT 1: UNRELATED SESSIONS CONTINUE. ──\n    assert!(\n        u_baseline > 0,\n        \"INVARIANT 1: the unrelated session's viewer must have been receiving output \\\n         BEFORE the incident (else the 'kept ticking' proof is vacuous); got \\\n         u_baseline={u_baseline}\"\n    );\n    assert!(\n        u_final > u_baseline,\n        \"INVARIANT 1: the UNRELATED session's viewer must keep receiving output \\\n         ACROSS the black-hole incident — the fan-out must never block the whole \\\n         broker on the one wedged conn; got u_baseline={u_baseline} u_final={u_final}\"\n    );\n\n    // ── INVARIANT 2: THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE\n    //    BOUND — a write on the OLD client conn FAILS (it was physically retired,\n    //    not merely logically stall-evicted). ──\n    assert_eq!(\n        old_client_write_is_err,\n        Some(true),\n        \"INVARIANT 2: a write on the OLD black-holed client connection must FAIL — \\\n         the physical conn must be canceled/closed within the write bound of logical \\\n         release. A success means the conn is still physically alive (the r4 RED).\"\n    );\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\nuse std::sync::atomic::{AtomicU32, AtomicU64, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, encode_bytes, InputReq, OutputEvent, SessionsReply, SpawnReq, Spawned,\n    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::AttachIntent;\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-blackhole-lc-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\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\nfn kill_pid(pid: u32) {\n    let _ = std::process::Command::new(\"taskkill\")\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n        .output();\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 connect(name: &str) -> Stream {\n    for _ in 0..200 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n/// session so its viewer has a continuous frame stream to keep ticking on\n/// throughout the black-hole incident (invariant 1).\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n        program: \"cmd\".to_string(),\n        args: vec![\n            \"/c\".to_string(),\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\n        ],\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/// A real PTY child (PowerShell) that emits `SEED`, parks on a first `Read-Host`,\n/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\nfn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n        program: \"powershell\".to_string(),\n        args: vec![\n            \"-NoProfile\".to_string(),\n            \"-Command\".to_string(),\n            \"Write-Output SEED; $null = Read-Host; \\\n             foreach ($i in 1..1400) { Write-Output ('B' * 1023) }; $null = Read-Host\"\n                .to_string(),\n        ],\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/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\nfn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n    let mut c = connect(name);\n    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n    let sid = loop {\n        let f = read_frame(&mut c).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    (c, sid)\n}\n\n/// Query the broker's stall-evict tally. Bounded thread + channel (a wedge\n/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n    let name = name.to_string();\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n                    let _ = tx.send(r.count);\n                    return;\n                }\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = tx.send(0);\n                    return;\n                }\n            }\n        }\n    });\n    rx.recv_timeout(deadline).unwrap_or(0)\n}\n\n/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\nfn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n    let name = name.to_string();\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(&mut c, KIND_SESSIONS, serde_json::Value::Null);\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_SESSIONS_REPLY => {\n                    let r: SessionsReply = serde_json::from_value(f.payload).unwrap();\n                    let _ = tx.send(r);\n                    return;\n                }\n                Ok(_) => continue,\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…\n    });\n…\n    thread::spawn(move || {\n…\n        while Instant::now() < deadline && (outcome.is_none() || !got_output) {\n            match read_frame(&mut c) {\n…\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    if !got_output {\n                        let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n                        first_seq = Some(ev.seq);\n                        got_output = true;\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        let _ = fv_tx.send((outcome, got_output, first_seq));\n    });\n    let (fv_outcome, fv_got_output, fv_first_seq) = fv_rx\n        .recv_timeout(Duration::from_secs(18))\n        .unwrap_or((None, false, None));\n\n    // ── INVARIANT 1 final sample: the unrelated viewer's frame count must have\n    //    ADVANCED across the whole incident window (baseline → now). It kept\n    //    receiving output while B's controller conn was black-holed + retired. ──\n    let u_final = u_frames.load(Ordering::Relaxed);\n\n    // ── Teardown BEFORE asserting (so a failing assert leaves no orphan child):\n    //    kill both PTY children, stop the broker, drop the black-holed Stream. ──\n    if let Some(pid) = broker.session_pid(sid_b) {\n        kill_pid(pid);\n    }\n    if let Some(pid) = broker.session_pid(sid_u) {\n        kill_pid(pid);\n    }\n    broker.stop();\n    drop(r_stream);\n\n    eprintln!(\n        \"=== BLACKHOLE-LC HARNESS: saw_seed={saw_seed} seed_high={seed_high} \\\n         max_drained_seq={max_drained_seq} burst_advanced={burst_advanced} \\\n         u_baseline={u_baseline} u_at_release={u_at_release} u_final={u_final} \\\n         stall_evicts_seen={stall_evicts_seen} \\\n         handler_released={handler_released} old_client_write_is_err={old_client_write_is_err:?} \\\n         control_plane_ok={control_plane_ok} control_plane_elapsed={control_plane_elapsed:?} \\\n         resume_seq={resume_seq} fv_outcome={fv_outcome:?} fv_got_output={fv_got_output} \\\n         fv_first_seq={fv_first_seq:?} ===\"\n    );\n\n    // ── HARNESS preconditions (test-environment, distinct from the product\n    //    invariants): the black-hole must have actually been established, else the\n    //    five-invariant observation is vacuous. ──\n    assert!(\n        old_client_write_is_err.is_some(),\n        \"HARNESS: the black-holed controller never handed its stream back — the \\\n         PowerShell child likely never produced SEED (saw_seed={saw_seed}). This is a \\\n         test-environment failure, not a product regression.\"\n    );\n    assert!(\n        burst_advanced,\n        \"HARNESS: the victim PTY/ring high-water must advance ≥64 frames past the \\\n         burst trigger (seed_high={seed_high}) — the finite burst must actually flow.\"\n    );\n    assert!(\n        max_drained_seq > 0,\n        \"HARNESS: the controller must have drained a real seq>0 frame before \\\n         black-holing (load-gated repro); got max_drained_seq={max_drained_seq}\"\n    );\n    assert!(\n        (1..=2).contains(&stall_evicts_seen),\n        \"HARNESS: the one wedged controller's release must be tallied once (logical \\\n         reap OR physical poison) or twice (both raced — poison-beats-evict, \\\n         field-accepted v0.30.6); got {stall_evicts_seen}\"\n    );\n\n    // ── INVARIANT 1: UNRELATED SESSIONS CONTINUE. ──\n    assert!(\n        u_baseline > 0,\n        \"INVARIANT 1: the unrelated session's viewer must have been receiving output \\\n         BEFORE the incident (else the 'kept ticking' proof is vacuous); got \\\n         u_baseline={u_baseline}\"\n    );\n    assert!(\n        u_final > u_baseline,\n        \"INVARIANT 1: the UNRELATED session's viewer must keep receiving output \\\n         ACROSS the black-hole incident — the fan-out must never block the whole \\\n         broker on the one wedged conn; got u_baseline={u_baseline} u_final={u_final}\"\n    );\n\n    // ── INVARIANT 2: THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE\n    //    BOUND — a write on the OLD client conn FAILS (it was physically retired,\n    //    not merely logically stall-evicted). ──\n    assert_eq!(\n        old_client_write_is_err,\n        Some(true),\n        \"INVARIANT 2: a write on the OLD black-holed client connection must FAIL — \\\n         the physical conn must be canceled/closed within the write bound of logical \\\n         release. A success means the conn is still physically alive (the r4 RED).\"\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,null,390,null,531,null,552,553,null,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,709]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\conn_blackhole_lifecycle.rs"}}},"isError":false,"timestamp":1784195843524}}
{"type":"message","id":"d2529fbd","parentId":"b7865475","timestamp":"2026-07-16T09:57:23.530Z","message":{"role":"toolResult","toolCallId":"call_EWnhhR0cwB75NlJnzupGNEE8|fc_0b42584a8ab0545d016a58ab03793c8199a26a86cb15e21154","toolName":"grep","content":[{"type":"text","text":" 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...\n 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 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 45|\n*46|/// A message from the stdin reader thread to the pump.\n 47|enum StdinMsg {\n 48|    /// Raw bytes to forward to the session as `AttachRecord::Input`.\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...\n 59|/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\n*60|/// (scanning output); the Windows stdin reader READS them to decide whether to\n 61|/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\n 62|/// all platforms (the scan is cheap + keeps the model correct); only the Windows\n 63|/// reader acts on it (Unix terminals scroll natively, no capture).\n...\n 273|\n*274|    /// Tear the status down on detach: reset the scroll region to the full screen\n 275|    /// (`ESC[r`), home to row 1, clear it — so the terminal is clean afterward.\n 276|    // [impl->REQ-RC-IDENTITY]\n 277|    fn restore_bytes() -> Vec<u8> {\n...\n 458|/// Mint operator op ids from a random 64-bit base so two `spt rc` sessions never\n*459|/// collide in the broker's net-op journal (a journaled `send_attach_input` op is\n 460|/// exactly-once at the PTY — a reused id from a prior session would be deduped,\n 461|/// dropping the keystroke). Random base + monotonic counter.\n 462|struct OpMinter(AtomicU64);\n...\n 483|/// against a known-correct xterm table; an unknown key maps to empty (dropped,\n*484|/// never garbage). Ctrl+B is the detach prefix, intercepted in [`key_event_step`]\n 485|/// BEFORE this, so it never arrives here as `^B`.\n 486|// [impl->REQ-RC-KEY-VT-TRANSLATE]\n 487|#[cfg(windows)]\n...\n 499|            // Ctrl+<letter> → the C0 control byte (^A=1 … ^Z=26). Ctrl+B is the\n*500|            // detach prefix (consumed upstream), so it never reaches here.\n 501|            if ctrl {\n 502|                let lc = c.to_ascii_lowercase();\n 503|                if lc.is_ascii_alphabetic() {\n...\n 606|\n*607|/// The outcome of stepping the event-sourced detach SM over one key event.\n 608|#[cfg(windows)]\n 609|enum KeyAction {\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 617|\n*618|/// Step the Option-B `ctrl-b d` detach SM over one Windows key event — mirrors\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 624|// [impl->REQ-RC-KEY-VT-TRANSLATE]\n 625|#[cfg(windows)]\n 626|fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {\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 637|        }\n*638|        let mut bytes = vec![DETACH_PREFIX];\n 639|        bytes.extend(translate_key_event(ke));\n 640|        return KeyAction::Forward(bytes);\n 641|    }\n...\n 917|\n*918|/// What one stdin chunk yielded after the detach state machine: the bytes to\n*919|/// forward to the session, and whether the detach keybind fired.\n*920|struct DetachParse {\n 921|    forward: Vec<u8>,\n*922|    detach: bool,\n 923|}\n 924|\n*925|/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds\n 926|/// the \"saw the prefix\" state across chunk boundaries (a prefix can end one read\n*927|/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →\n 928|/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other\n*929|/// byte passes through. Pure so it unit-tests without a terminal. On detach the\n 930|/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the\n 931|/// viewport is ending).\n*932|fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n 933|    let mut forward = Vec::with_capacity(input.len());\n 934|    for &b in input {\n 935|        if *armed {\n...\n 937|            match b {\n*938|                DETACH_KEY => return DetachParse { forward, detach: true },\n*939|                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n 940|                other => {\n*941|                    forward.push(DETACH_PREFIX);\n 942|                    forward.push(other);\n 943|                }\n 944|            }\n*945|        } else if b == DETACH_PREFIX {\n 946|            *armed = true;\n 947|        } else {\n 948|            forward.push(b);\n...\n 950|    }\n*951|    DetachParse {\n 952|        forward,\n*953|        detach: false,\n 954|    }\n 955|}\n 956|\n*957|/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY\n 958|/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the\n 959|/// special keys the legacy console delivers as events — arrows / Home / End /\n 960|/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected\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 967|        use std::io::IsTerminal;\n 968|        if std::io::stdin().is_terminal() {\n*969|            spawn_stdin_reader_events(tx, mouse_mode);\n 970|            return;\n 971|        }\n 972|    }\n...\n 974|    let _ = &mouse_mode;\n*975|    spawn_stdin_reader_bytes(tx);\n 976|}\n 977|\n*978|/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback\n*979|/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach\n 980|/// SM. No key translation — a non-tty stream carries bytes, not console key events.\n*981|fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {\n 982|    std::thread::spawn(move || {\n 983|        let mut stdin = std::io::stdin().lock();\n 984|        let mut buf = [0u8; 1024];\n*985|        let mut armed = false; // saw the detach prefix, awaiting the next byte\n 986|        loop {\n*987|            let n = match stdin.read(&mut buf) {\n 988|                Ok(0) => break, // stdin closed\n 989|                Ok(n) => n,\n 990|                Err(_) => break,\n...\n 999|            }\n*1000|            if parsed.detach {\n*1001|                let _ = tx.send(StdinMsg::Detach);\n 1002|                return;\n 1003|            }\n 1004|        }\n...\n 1008|/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the\n*1009|/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /\n 1010|/// Release dropped to avoid double-input); a bracketed Paste forwards its content\n 1011|/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.\n 1012|/// `mouse_mode` (written by the pump from the harness's output DECSET) gates\n...\n 1014|#[cfg(windows)]\n*1015|fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n 1016|    use crossterm::event::{self, Event, KeyEventKind};\n 1017|    use std::sync::atomic::Ordering::Acquire;\n 1018|    std::thread::spawn(move || {\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 1078|/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n*1079|/// takes control. Returns when the session exits, the operator detaches, or an\n 1080|/// error occurs.\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...\n 1186|    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n*1187|    // pump and read by the Windows stdin reader to gate scroll forwarding\n 1188|    // (REQ-RC-MOUSE-FORWARD).\n 1189|    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n 1190|    let rx = if view {\n...\n 1193|        let (tx, rx) = mpsc::channel();\n*1194|        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n 1195|        Some(rx)\n 1196|    };\n 1197|\n...\n 1228|    if status.is_some() {\n*1229|        let _ = stdout.write_all(&StatusRow::restore_bytes());\n 1230|        let _ = stdout.flush();\n 1231|    }\n 1232|    // Out of raw mode (guard drops after) before the parting line.\n...\n 1238|        }\n*1239|        Ok(PumpEnd::Detached) => {\n*1240|            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n 1241|            Ok(())\n 1242|        }\n 1243|        Ok(PumpEnd::Displaced(by)) => {\n...\n 1277|        // Reconnectable ends never escape the loop (they enter the retry arm and\n*1278|        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n 1279|        // so a future PumpEnd variant still forces a decision here.\n 1280|        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n 1281|            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n...\n 1289|                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n*1290|                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n 1291|                 `spt endpoint run` to restart it]\"\n 1292|            );\n 1293|            Ok(())\n...\n 1511|/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n*1512|/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n 1513|/// construction/teardown, the parting copy. This fn only needs an established\n 1514|/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n 1515|/// (`out` = a capture buffer) — no behavior change for the interactive path.\n...\n 1570|        // detail stays off the terminal per the Q4 UX rule), then re-drive\n*1571|        // establish_attach inside the bounded window. A detach keypress mid-window\n 1572|        // aborts the retry loop honestly. The banner repaints each iteration with a\n 1573|        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n 1574|        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n...\n 1585|            let remaining = reconnect_remaining_secs(elapsed);\n*1586|            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n 1587|            let _ = out.flush();\n*1588|            // Operator gives up mid-reconnect: honor the detach keys.\n 1589|            if let Some(rx) = rx {\n*1590|                let mut detached = false;\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*1596|                if detached {\n*1597|                    break Some(Err(())); // detach sentinel\n 1598|                }\n 1599|            }\n 1600|            match establish_attach(endpoint_id, intent) {\n...\n 1614|            }\n*1615|            Some(Err(())) => return Ok(PumpEnd::Detached),\n 1616|            None => {\n 1617|                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n 1618|                // daemon (only knowable for a LOCAL target) gets the loud \"session\n...\n 1666|/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n*1667|/// Everything else — session exit, operator detach, displacement, the stall\n 1668|/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n 1669|/// session the operator (or the session itself) deliberately ended.\n 1670|// [impl->REQ-RC-RECONNECT]\n...\n 1674|        PumpEnd::Exited(_)\n*1675|        | PumpEnd::Detached\n 1676|        | PumpEnd::Displaced(_)\n 1677|        | PumpEnd::Stalled\n 1678|        | PumpEnd::NoLiveSession\n...\n 1685|/// REFUSED the attach (no live session / gate deny — the cross-node D6\n*1686|/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n 1687|/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n 1688|// [impl->REQ-RC-RECONNECT]\n 1689|fn serve_eof_end(rendered_any: bool) -> PumpEnd {\n...\n 1721|    Exited(Option<i32>),\n*1722|    Detached,\n 1723|    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n 1724|    Displaced(String),\n 1725|    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n...\n 1730|    /// The serve side finished the stream AFTER rendering output — the remote\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 1736|    Severed,\n 1737|    /// B-3 terminal state: the reconnect window expired without a successful\n...\n 1756|    /// the D6 stale-row case: the registry routed us to a node that no longer has\n*1757|    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n 1758|    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n*1759|    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n 1760|    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n 1761|    NoLiveSession,\n 1762|}\n...\n 1808|\n*1809|/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1810|/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1811|#[allow(clippy::too_many_arguments)]\n 1812|fn pump(\n 1813|    brain: &mut Brain,\n...\n 1823|    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n*1824|    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n 1825|    // scanner is pump-local (single producer) and survives a sequence split across\n 1826|    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n 1827|    let mut mouse_scanner = MouseModeScanner::default();\n...\n 1836|        stdout\n*1837|            .write_all(&status.assert_bytes())\n 1838|            .map_err(|e| format!(\"status assert: {e}\"))?;\n 1839|        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n 1840|    }\n*1841|    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n 1842|    // (the session outlives the viewport). Best-effort: an already-finished or\n 1843|    // exited stream send is harmless to attempt.\n*1844|    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n 1845|        let _ = brain.net_stream_send(stream_id, &[], None, true);\n*1846|        PumpEnd::Detached\n 1847|    }\n 1848|    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n 1849|    // drop any `seq` already rendered (the operator render cursor, matching the\n...\n 1854|    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n*1855|    // raw-stdin reader). `None` until the first read.\n 1856|    let mut last_size: Option<(u16, u16)> = None;\n 1857|    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n 1858|    // as a one-line indicator when it differs from our own terminal.\n...\n 1866|    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n*1867|    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n 1868|    // Any live session replays its buffered ring on attach, so a real attach sets\n 1869|    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n 1870|    let mut rendered_any = false;\n...\n 1884|                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n*1885|                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n 1886|                                let _ = stdout.flush();\n 1887|                            }\n 1888|                        }\n...\n 1901|                    Ok(StdinMsg::Bytes(bytes)) => {\n*1902|                        send_attach_input(brain, stream_id, &bytes, ops.next())\n 1903|                            .map_err(|e| format!(\"send input: {e}\"))?;\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 1909|                    }\n 1910|                }\n 1911|            }\n...\n 1948|                            mouse_scanner.feed(mouse_mode, &bytes);\n*1949|                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n 1950|                            // Re-assert the reserved row if the harness output just\n 1951|                            // destroyed our scroll region — alt-screen enter or a\n 1952|                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n...\n 1960|                                    stdout\n*1961|                                        .write_all(&status.assert_bytes())\n 1962|                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n 1963|                                }\n 1964|                            }\n...\n 2006|        let p = parse_stdin_chunk(armed, input);\n*2007|        (p.forward, p.detach)\n 2008|    }\n 2009|\n 2010|    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n...\n 2052|    // (REQ-RC-KEY-VT-TRANSLATE supersedes the W7 normalize_key_byte swap on Windows:\n*2053|    // the exhaustive translate_key_event mapping unit + the event-detach unit live\n 2054|    // alongside this module's tests; Backspace→0x7f / Ctrl+Backspace→0x08 carry the\n 2055|    // relocated [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] evidence there.)\n 2056|\n...\n 2251|\n*2252|        // Drive the REAL loop headless: viewer intent (no stdin reader, no\n 2253|        // status row), the capture buffer as the out sink.\n 2254|        let cap = Capture::default();\n 2255|        let mut cap_out = cap.clone();\n...\n 2385|    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\n*2386|    // drop). Session exit / operator detach / displacement / the stall backstop /\n 2387|    // an attach refuse are FINAL: re-driving those would re-attach to a session\n 2388|    // the operator (or the session itself) deliberately ended.\n 2389|    #[test]\n...\n 2393|        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n*2394|        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n 2395|        assert_eq!(\n 2396|            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n 2397|            EndClass::Final\n...\n 2411|    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\n*2412|    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\n 2413|    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\n 2414|    #[test]\n 2415|    fn serve_eof_severed_vs_refused() {\n...\n 2618|\n*2619|    // [unit->REQ-RC-1] the detach keybind state machine: `ctrl-b d` detaches\n 2620|    // (the prefix is NOT forwarded); `ctrl-b ctrl-b` forwards one literal\n 2621|    // ctrl-b (so the session can still receive it); `ctrl-b <other>` forwards\n 2622|    // prefix + other verbatim; plain input passes through untouched.\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 2628|        assert!(!armed);\n*2629|        // ctrl-b d => detach, bytes-before forwarded, prefix consumed\n 2630|        assert_eq!(\n*2631|            parse(&mut armed, &[b'h', DETACH_PREFIX, DETACH_KEY]),\n 2632|            (b\"h\".to_vec(), true)\n 2633|        );\n*2634|        // ctrl-b ctrl-b => one literal ctrl-b, no detach\n 2635|        let mut a2 = false;\n 2636|        assert_eq!(\n*2637|            parse(&mut a2, &[DETACH_PREFIX, DETACH_PREFIX]),\n*2638|            (vec![DETACH_PREFIX], false)\n 2639|        );\n 2640|        // ctrl-b <other> => prefix + other verbatim\n 2641|        let mut a3 = false;\n 2642|        assert_eq!(\n*2643|            parse(&mut a3, &[DETACH_PREFIX, b'x']),\n*2644|            (vec![DETACH_PREFIX, b'x'], false)\n 2645|        );\n 2646|    }\n 2647|\n 2648|    // [unit->REQ-RC-1] the prefix may straddle a read boundary: a chunk ending\n*2649|    // in ctrl-b arms across chunks, and the next chunk's `d` detaches — so the\n 2650|    // keybind works no matter how raw reads fragment it.\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 2656|        assert!(armed, \"prefix armed across the chunk boundary\");\n*2657|        // chunk 2 starts with the detach key: detach fires.\n*2658|        assert_eq!(parse(&mut armed, &[DETACH_KEY]), (Vec::new(), true));\n 2659|    }\n 2660|\n 2661|    // [unit->REQ-RC-1] operator op ids are non-colliding across sessions: two\n*2662|    // minters seed from distinct random bases (so a journaled send_attach_input\n 2663|    // op from a prior rc session is never deduped as a replay), and each minter\n 2664|    // is strictly monotonic within a session.\n 2665|    #[test]\n...\n 2790|\n*2791|    // [unit->REQ-RC-KEY-VT-TRANSLATE] The event-sourced Option-B `ctrl-b d` detach\n*2792|    // SM (key-event path), mirroring the byte-path `detach_keybind_semantics`:\n*2793|    // Ctrl+B arms (swallowed); armed + plain 'd' detaches; armed + Ctrl+B forwards a\n 2794|    // literal 0x02; armed + other forwards 0x02 then the translated event; and\n*2795|    // critically the detach key is PLAIN 'd' only — Ctrl+D does NOT detach (it\n 2796|    // forwards 0x02 then its 0x04 translation). `armed` is mutated in place.\n 2797|    #[cfg(windows)]\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 2812|\n*2813|        // armed + plain 'd' → Detach.\n 2814|        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {\n*2815|            KeyAction::Detach => {}\n*2816|            other => panic!(\"armed + 'd' should Detach, got {:?}\", action_name(&other)),\n 2817|        }\n*2818|        assert!(!armed, \"Detach disarms\");\n 2819|\n 2820|        // armed + Ctrl+B → Forward(literal 0x02), disarms.\n 2821|        armed = true;\n...\n 2835|\n*2836|        // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.\n 2837|        armed = true;\n 2838|        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {\n 2839|            KeyAction::Forward(v) => assert_eq!(\n...\n 2841|                vec![0x02, 0x04],\n*2842|                \"Ctrl+D must forward prefix + ^D, never detach\"\n 2843|            ),\n*2844|            other => panic!(\"armed + Ctrl+D must NOT Detach, got {:?}\", action_name(&other)),\n 2845|        }\n 2846|        assert!(!armed);\n 2847|\n...\n 2860|        match a {\n*2861|            KeyAction::Detach => \"Detach\",\n 2862|            KeyAction::Forward(_) => \"Forward\",\n 2863|            KeyAction::Swallow => \"Swallow\",\n 2864|        }\n...\n 3173|        // (a) pump START: reserve + paint.\n*3174|        sink.write_all(&status.assert_bytes()).unwrap();\n 3175|        let assert_len = sink.len();\n 3176|        assert!(sink.starts_with(b\"\\x1b[2;24r\"), \"start reserves the row\");\n 3177|        assert!(\n...\n 3184|        if scanner.feed(plain) {\n*3185|            sink.write_all(&status.assert_bytes()).unwrap();\n 3186|        }\n 3187|        assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");\n 3188|\n...\n 3196|            fired = true;\n*3197|            sink.write_all(&status.assert_bytes()).unwrap();\n 3198|        }\n 3199|        assert!(fired, \"the alt-screen enter trips the re-assert scanner\");\n 3200|        assert!(sink.len() > before, \"a re-assert was appended after the trigger\");\n...\n 3206|        // (c) pump EXIT: restore (reset region + clear row 1).\n*3207|        sink.write_all(&StatusRow::restore_bytes()).unwrap();\n 3208|        assert!(\n 3209|            sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),\n 3210|            \"exit restores the full scroll region and clears the reserved row\""}],"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":138,"fileCount":1,"files":["crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":138}],"truncated":false,"displayContent":"   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     │...\n   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   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   45│\n  *46│/// A message from the stdin reader thread to the pump.\n   47│enum StdinMsg {\n   48│    /// Raw bytes to forward to the session as `AttachRecord::Input`.\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     │...\n   59│/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\n  *60│/// (scanning output); the Windows stdin reader READS them to decide whether to\n   61│/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\n   62│/// all platforms (the scan is cheap + keeps the model correct); only the Windows\n   63│/// reader acts on it (Unix terminals scroll natively, no capture).\n     │...\n  273│\n *274│    /// Tear the status down on detach: reset the scroll region to the full screen\n  275│    /// (`ESC[r`), home to row 1, clear it — so the terminal is clean afterward.\n  276│    // [impl->REQ-RC-IDENTITY]\n  277│    fn restore_bytes() -> Vec<u8> {\n     │...\n  458│/// Mint operator op ids from a random 64-bit base so two `spt rc` sessions never\n *459│/// collide in the broker's net-op journal (a journaled `send_attach_input` op is\n  460│/// exactly-once at the PTY — a reused id from a prior session would be deduped,\n  461│/// dropping the keystroke). Random base + monotonic counter.\n  462│struct OpMinter(AtomicU64);\n     │...\n  483│/// against a known-correct xterm table; an unknown key maps to empty (dropped,\n *484│/// never garbage). Ctrl+B is the detach prefix, intercepted in [`key_event_step`]\n  485│/// BEFORE this, so it never arrives here as `^B`.\n  486│// [impl->REQ-RC-KEY-VT-TRANSLATE]\n  487│#[cfg(windows)]\n     │...\n  499│            // Ctrl+<letter> → the C0 control byte (^A=1 … ^Z=26). Ctrl+B is the\n *500│            // detach prefix (consumed upstream), so it never reaches here.\n  501│            if ctrl {\n  502│                let lc = c.to_ascii_lowercase();\n  503│                if lc.is_ascii_alphabetic() {\n     │...\n  606│\n *607│/// The outcome of stepping the event-sourced detach SM over one key event.\n  608│#[cfg(windows)]\n  609│enum KeyAction {\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  617│\n *618│/// Step the Option-B `ctrl-b d` detach SM over one Windows key event — mirrors\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  624│// [impl->REQ-RC-KEY-VT-TRANSLATE]\n  625│#[cfg(windows)]\n  626│fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {\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  637│        }\n *638│        let mut bytes = vec![DETACH_PREFIX];\n  639│        bytes.extend(translate_key_event(ke));\n  640│        return KeyAction::Forward(bytes);\n  641│    }\n     │...\n  917│\n *918│/// What one stdin chunk yielded after the detach state machine: the bytes to\n *919│/// forward to the session, and whether the detach keybind fired.\n *920│struct DetachParse {\n  921│    forward: Vec<u8>,\n *922│    detach: bool,\n  923│}\n  924│\n *925│/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds\n  926│/// the \"saw the prefix\" state across chunk boundaries (a prefix can end one read\n *927│/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →\n  928│/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other\n *929│/// byte passes through. Pure so it unit-tests without a terminal. On detach the\n  930│/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the\n  931│/// viewport is ending).\n *932│fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n  933│    let mut forward = Vec::with_capacity(input.len());\n  934│    for &b in input {\n  935│        if *armed {\n     │...\n  937│            match b {\n *938│                DETACH_KEY => return DetachParse { forward, detach: true },\n *939│                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n  940│                other => {\n *941│                    forward.push(DETACH_PREFIX);\n  942│                    forward.push(other);\n  943│                }\n  944│            }\n *945│        } else if b == DETACH_PREFIX {\n  946│            *armed = true;\n  947│        } else {\n  948│            forward.push(b);\n     │...\n  950│    }\n *951│    DetachParse {\n  952│        forward,\n *953│        detach: false,\n  954│    }\n  955│}\n  956│\n *957│/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY\n  958│/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the\n  959│/// special keys the legacy console delivers as events — arrows / Home / End /\n  960│/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected\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  967│        use std::io::IsTerminal;\n  968│        if std::io::stdin().is_terminal() {\n *969│            spawn_stdin_reader_events(tx, mouse_mode);\n  970│            return;\n  971│        }\n  972│    }\n     │...\n  974│    let _ = &mouse_mode;\n *975│    spawn_stdin_reader_bytes(tx);\n  976│}\n  977│\n *978│/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback\n *979│/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach\n  980│/// SM. No key translation — a non-tty stream carries bytes, not console key events.\n *981│fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {\n  982│    std::thread::spawn(move || {\n  983│        let mut stdin = std::io::stdin().lock();\n  984│        let mut buf = [0u8; 1024];\n *985│        let mut armed = false; // saw the detach prefix, awaiting the next byte\n  986│        loop {\n *987│            let n = match stdin.read(&mut buf) {\n  988│                Ok(0) => break, // stdin closed\n  989│                Ok(n) => n,\n  990│                Err(_) => break,\n     │...\n  999│            }\n*1000│            if parsed.detach {\n*1001│                let _ = tx.send(StdinMsg::Detach);\n 1002│                return;\n 1003│            }\n 1004│        }\n     │...\n 1008│/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the\n*1009│/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /\n 1010│/// Release dropped to avoid double-input); a bracketed Paste forwards its content\n 1011│/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.\n 1012│/// `mouse_mode` (written by the pump from the harness's output DECSET) gates\n     │...\n 1014│#[cfg(windows)]\n*1015│fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n 1016│    use crossterm::event::{self, Event, KeyEventKind};\n 1017│    use std::sync::atomic::Ordering::Acquire;\n 1018│    std::thread::spawn(move || {\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 1078│/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n*1079│/// takes control. Returns when the session exits, the operator detaches, or an\n 1080│/// error occurs.\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     │...\n 1186│    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n*1187│    // pump and read by the Windows stdin reader to gate scroll forwarding\n 1188│    // (REQ-RC-MOUSE-FORWARD).\n 1189│    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n 1190│    let rx = if view {\n     │...\n 1193│        let (tx, rx) = mpsc::channel();\n*1194│        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n 1195│        Some(rx)\n 1196│    };\n 1197│\n     │...\n 1228│    if status.is_some() {\n*1229│        let _ = stdout.write_all(&StatusRow::restore_bytes());\n 1230│        let _ = stdout.flush();\n 1231│    }\n 1232│    // Out of raw mode (guard drops after) before the parting line.\n     │...\n 1238│        }\n*1239│        Ok(PumpEnd::Detached) => {\n*1240│            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n 1241│            Ok(())\n 1242│        }\n 1243│        Ok(PumpEnd::Displaced(by)) => {\n     │...\n 1277│        // Reconnectable ends never escape the loop (they enter the retry arm and\n*1278│        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n 1279│        // so a future PumpEnd variant still forces a decision here.\n 1280│        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n 1281│            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n     │...\n 1289│                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n*1290│                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n 1291│                 `spt endpoint run` to restart it]\"\n 1292│            );\n 1293│            Ok(())\n     │...\n 1511│/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n*1512│/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n 1513│/// construction/teardown, the parting copy. This fn only needs an established\n 1514│/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n 1515│/// (`out` = a capture buffer) — no behavior change for the interactive path.\n     │...\n 1570│        // detail stays off the terminal per the Q4 UX rule), then re-drive\n*1571│        // establish_attach inside the bounded window. A detach keypress mid-window\n 1572│        // aborts the retry loop honestly. The banner repaints each iteration with a\n 1573│        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n 1574│        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n     │...\n 1585│            let remaining = reconnect_remaining_secs(elapsed);\n*1586│            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n 1587│            let _ = out.flush();\n*1588│            // Operator gives up mid-reconnect: honor the detach keys.\n 1589│            if let Some(rx) = rx {\n*1590│                let mut detached = false;\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*1596│                if detached {\n*1597│                    break Some(Err(())); // detach sentinel\n 1598│                }\n 1599│            }\n 1600│            match establish_attach(endpoint_id, intent) {\n     │...\n 1614│            }\n*1615│            Some(Err(())) => return Ok(PumpEnd::Detached),\n 1616│            None => {\n 1617│                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n 1618│                // daemon (only knowable for a LOCAL target) gets the loud \"session\n     │...\n 1666│/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n*1667│/// Everything else — session exit, operator detach, displacement, the stall\n 1668│/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n 1669│/// session the operator (or the session itself) deliberately ended.\n 1670│// [impl->REQ-RC-RECONNECT]\n     │...\n 1674│        PumpEnd::Exited(_)\n*1675│        | PumpEnd::Detached\n 1676│        | PumpEnd::Displaced(_)\n 1677│        | PumpEnd::Stalled\n 1678│        | PumpEnd::NoLiveSession\n     │...\n 1685│/// REFUSED the attach (no live session / gate deny — the cross-node D6\n*1686│/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n 1687│/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n 1688│// [impl->REQ-RC-RECONNECT]\n 1689│fn serve_eof_end(rendered_any: bool) -> PumpEnd {\n     │...\n 1721│    Exited(Option<i32>),\n*1722│    Detached,\n 1723│    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n 1724│    Displaced(String),\n 1725│    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n     │...\n 1730│    /// The serve side finished the stream AFTER rendering output — the remote\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 1736│    Severed,\n 1737│    /// B-3 terminal state: the reconnect window expired without a successful\n     │...\n 1756│    /// the D6 stale-row case: the registry routed us to a node that no longer has\n*1757│    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n 1758│    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n*1759│    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n 1760│    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n 1761│    NoLiveSession,\n 1762│}\n     │...\n 1808│\n*1809│/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1810│/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1811│#[allow(clippy::too_many_arguments)]\n 1812│fn pump(\n 1813│    brain: &mut Brain,\n     │...\n 1823│    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n*1824│    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n 1825│    // scanner is pump-local (single producer) and survives a sequence split across\n 1826│    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n 1827│    let mut mouse_scanner = MouseModeScanner::default();\n     │...\n 1836│        stdout\n*1837│            .write_all(&status.assert_bytes())\n 1838│            .map_err(|e| format!(\"status assert: {e}\"))?;\n 1839│        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n 1840│    }\n*1841│    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n 1842│    // (the session outlives the viewport). Best-effort: an already-finished or\n 1843│    // exited stream send is harmless to attempt.\n*1844│    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n 1845│        let _ = brain.net_stream_send(stream_id, &[], None, true);\n*1846│        PumpEnd::Detached\n 1847│    }\n 1848│    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n 1849│    // drop any `seq` already rendered (the operator render cursor, matching the\n     │...\n 1854│    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n*1855│    // raw-stdin reader). `None` until the first read.\n 1856│    let mut last_size: Option<(u16, u16)> = None;\n 1857│    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n 1858│    // as a one-line indicator when it differs from our own terminal.\n     │...\n 1866│    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n*1867│    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n 1868│    // Any live session replays its buffered ring on attach, so a real attach sets\n 1869│    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n 1870│    let mut rendered_any = false;\n     │...\n 1884│                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n*1885│                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n 1886│                                let _ = stdout.flush();\n 1887│                            }\n 1888│                        }\n     │...\n 1901│                    Ok(StdinMsg::Bytes(bytes)) => {\n*1902│                        send_attach_input(brain, stream_id, &bytes, ops.next())\n 1903│                            .map_err(|e| format!(\"send input: {e}\"))?;\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 1909│                    }\n 1910│                }\n 1911│            }\n     │...\n 1948│                            mouse_scanner.feed(mouse_mode, &bytes);\n*1949│                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n 1950│                            // Re-assert the reserved row if the harness output just\n 1951│                            // destroyed our scroll region — alt-screen enter or a\n 1952│                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n     │...\n 1960│                                    stdout\n*1961│                                        .write_all(&status.assert_bytes())\n 1962│                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n 1963│                                }\n 1964│                            }\n     │...\n 2006│        let p = parse_stdin_chunk(armed, input);\n*2007│        (p.forward, p.detach)\n 2008│    }\n 2009│\n 2010│    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n     │...\n 2052│    // (REQ-RC-KEY-VT-TRANSLATE supersedes the W7 normalize_key_byte swap on Windows:\n*2053│    // the exhaustive translate_key_event mapping unit + the event-detach unit live\n 2054│    // alongside this module's tests; Backspace→0x7f / Ctrl+Backspace→0x08 carry the\n 2055│    // relocated [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] evidence there.)\n 2056│\n     │...\n 2251│\n*2252│        // Drive the REAL loop headless: viewer intent (no stdin reader, no\n 2253│        // status row), the capture buffer as the out sink.\n 2254│        let cap = Capture::default();\n 2255│        let mut cap_out = cap.clone();\n     │...\n 2385│    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\n*2386│    // drop). Session exit / operator detach / displacement / the stall backstop /\n 2387│    // an attach refuse are FINAL: re-driving those would re-attach to a session\n 2388│    // the operator (or the session itself) deliberately ended.\n 2389│    #[test]\n     │...\n 2393│        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n*2394│        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n 2395│        assert_eq!(\n 2396│            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n 2397│            EndClass::Final\n     │...\n 2411│    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\n*2412│    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\n 2413│    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\n 2414│    #[test]\n 2415│    fn serve_eof_severed_vs_refused() {\n     │...\n 2618│\n*2619│    // [unit->REQ-RC-1] the detach keybind state machine: `ctrl-b d` detaches\n 2620│    // (the prefix is NOT forwarded); `ctrl-b ctrl-b` forwards one literal\n 2621│    // ctrl-b (so the session can still receive it); `ctrl-b <other>` forwards\n 2622│    // prefix + other verbatim; plain input passes through untouched.\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 2628│        assert!(!armed);\n*2629│        // ctrl-b d => detach, bytes-before forwarded, prefix consumed\n 2630│        assert_eq!(\n*2631│            parse(&mut armed, &[b'h', DETACH_PREFIX, DETACH_KEY]),\n 2632│            (b\"h\".to_vec(), true)\n 2633│        );\n*2634│        // ctrl-b ctrl-b => one literal ctrl-b, no detach\n 2635│        let mut a2 = false;\n 2636│        assert_eq!(\n*2637│            parse(&mut a2, &[DETACH_PREFIX, DETACH_PREFIX]),\n*2638│            (vec![DETACH_PREFIX], false)\n 2639│        );\n 2640│        // ctrl-b <other> => prefix + other verbatim\n 2641│        let mut a3 = false;\n 2642│        assert_eq!(\n*2643│            parse(&mut a3, &[DETACH_PREFIX, b'x']),\n*2644│            (vec![DETACH_PREFIX, b'x'], false)\n 2645│        );\n 2646│    }\n 2647│\n 2648│    // [unit->REQ-RC-1] the prefix may straddle a read boundary: a chunk ending\n*2649│    // in ctrl-b arms across chunks, and the next chunk's `d` detaches — so the\n 2650│    // keybind works no matter how raw reads fragment it.\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 2656│        assert!(armed, \"prefix armed across the chunk boundary\");\n*2657│        // chunk 2 starts with the detach key: detach fires.\n*2658│        assert_eq!(parse(&mut armed, &[DETACH_KEY]), (Vec::new(), true));\n 2659│    }\n 2660│\n 2661│    // [unit->REQ-RC-1] operator op ids are non-colliding across sessions: two\n*2662│    // minters seed from distinct random bases (so a journaled send_attach_input\n 2663│    // op from a prior rc session is never deduped as a replay), and each minter\n 2664│    // is strictly monotonic within a session.\n 2665│    #[test]\n     │...\n 2790│\n*2791│    // [unit->REQ-RC-KEY-VT-TRANSLATE] The event-sourced Option-B `ctrl-b d` detach\n*2792│    // SM (key-event path), mirroring the byte-path `detach_keybind_semantics`:\n*2793│    // Ctrl+B arms (swallowed); armed + plain 'd' detaches; armed + Ctrl+B forwards a\n 2794│    // literal 0x02; armed + other forwards 0x02 then the translated event; and\n*2795│    // critically the detach key is PLAIN 'd' only — Ctrl+D does NOT detach (it\n 2796│    // forwards 0x02 then its 0x04 translation). `armed` is mutated in place.\n 2797│    #[cfg(windows)]\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 2812│\n*2813│        // armed + plain 'd' → Detach.\n 2814│        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {\n*2815│            KeyAction::Detach => {}\n*2816│            other => panic!(\"armed + 'd' should Detach, got {:?}\", action_name(&other)),\n 2817│        }\n*2818│        assert!(!armed, \"Detach disarms\");\n 2819│\n 2820│        // armed + Ctrl+B → Forward(literal 0x02), disarms.\n 2821│        armed = true;\n     │...\n 2835│\n*2836│        // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.\n 2837│        armed = true;\n 2838│        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {\n 2839│            KeyAction::Forward(v) => assert_eq!(\n     │...\n 2841│                vec![0x02, 0x04],\n*2842│                \"Ctrl+D must forward prefix + ^D, never detach\"\n 2843│            ),\n*2844│            other => panic!(\"armed + Ctrl+D must NOT Detach, got {:?}\", action_name(&other)),\n 2845│        }\n 2846│        assert!(!armed);\n 2847│\n     │...\n 2860│        match a {\n*2861│            KeyAction::Detach => \"Detach\",\n 2862│            KeyAction::Forward(_) => \"Forward\",\n 2863│            KeyAction::Swallow => \"Swallow\",\n 2864│        }\n     │...\n 3173│        // (a) pump START: reserve + paint.\n*3174│        sink.write_all(&status.assert_bytes()).unwrap();\n 3175│        let assert_len = sink.len();\n 3176│        assert!(sink.starts_with(b\"\\x1b[2;24r\"), \"start reserves the row\");\n 3177│        assert!(\n     │...\n 3184│        if scanner.feed(plain) {\n*3185│            sink.write_all(&status.assert_bytes()).unwrap();\n 3186│        }\n 3187│        assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");\n 3188│\n     │...\n 3196│            fired = true;\n*3197│            sink.write_all(&status.assert_bytes()).unwrap();\n 3198│        }\n 3199│        assert!(fired, \"the alt-screen enter trips the re-assert scanner\");\n 3200│        assert!(sink.len() > before, \"a re-assert was appended after the trigger\");\n     │...\n 3206│        // (c) pump EXIT: restore (reset region + clear row 1).\n*3207│        sink.write_all(&StatusRow::restore_bytes()).unwrap();\n 3208│        assert!(\n 3209│            sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),\n 3210│            \"exit restores the full scroll region and clears the reserved row\""},"isError":false,"timestamp":1784195843530}}
{"type":"message","id":"081954b4","parentId":"d2529fbd","timestamp":"2026-07-16T09:57:23.535Z","message":{"role":"toolResult","toolCallId":"call_q5Ezx5awDfCMXgvCPIrGnmJr|fc_0b42584a8ab0545d016a58ab0379248199a69e3736d6dfe475","toolName":"read","content":[{"type":"text","text":"//! `mock-session` — the generic mock adapter's \"agent session\". A trivial,\n//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,\n//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's\n//! `[session.self]` template, it does what a real session's boot hook would do:\n//! fire `spt api bind` to flip its perch live, then `spt api state idle`.\n//!\n//! It shells out to the `spt` binary exactly as a real adapter would — resolved\n//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves\n//! the inbound contract is harness-agnostic: a dumb helper drives it with only\n//! `spt api` calls.\n\nuse std::process::Command;\n\nfn arg(args: &[String], flag: &str) -> Option<String> {\n    args.iter()\n        .position(|a| a == flag)\n        .and_then(|i| args.get(i + 1).cloned())\n}\n\nfn main() {\n    let args: Vec<String> = std::env::args().skip(1).collect();\n\n    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core\n    // binds its nested perch; the child just needs to launch). `echo` is the\n    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.\n    match arg(&args, \"--mode\").as_deref() {\n        Some(\"psyche\") => {\n            let id = arg(&args, \"--id\").unwrap_or_else(|| \"mock-psyche\".to_string());\n            println!(\"MOCK_PSYCHE_UP:{id}\");\n            return;\n        }\n        Some(\"echo\") => {\n            use std::io::Read;\n            let mut history = String::new();\n            let _ = std::io::stdin().read_to_string(&mut history);\n            // A deterministic brief; a real echo summarizes the history it read.\n            println!(\"mock echo brief ({} history bytes)\", history.len());\n            return;\n        }\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\n            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived\n            // stand-in for a real harness session. Binds its perch (the harness\n            // contract) exactly like the default stand-in, then prints a stdout\n            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL\n            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`\n            // attach path can be exercised end-to-end (the default mode exits\n            // immediately, which is why the v0.12.0 mock tests never caught the\n            // daemon+broker+PTY lifecycle defects). Isolates \"broker wedged /\n            // endpoint died\" from \"a real harness failed to launch.\"\n            //\n            // `--mode hold-unbound` (W6, REQ-ENDPOINT-UNBOUND-ATTACH): identical, but\n            // it NEVER binds — the broker session is live (PTY + heartbeat) while the\n            // perch stays the endpoint-run UNBOUND skeleton forever. That gives the\n            // int a deterministic, race-free live pre-bind session to prove the\n            // attach-before-bind contract (`spt rc` attaches a STATUS_UNBOUND\n            // endpoint) + the real picker Unbound render against a REAL session. The\n            // UNBOUND→ONLINE flip is proven separately by the normal `dummy` bringup.\n            let no_bind = mode == \"hold-unbound\";\n            use std::io::Write;\n            let id = arg(&args, \"--id\").unwrap_or_else(|| {\n                eprintln!(\"mock-session --mode dummy: --id required\");\n                std::process::exit(2);\n            });\n            let session_id =\n                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n            let spt = arg(&args, \"--spt\")\n                .or_else(|| std::env::var(\"SPT_BIN\").ok())\n                .unwrap_or_else(|| \"spt\".to_string());\n            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n            let endpoint_type = arg(&args, \"--type\");\n            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\n                .and_then(|s| s.parse().ok())\n                .unwrap_or(500);\n\n            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when\n            // `--marker-file <path>` is set, write a marker whose CONTENTS record\n            // (a) which session template launched this dummy — `resume` if the\n            // command carried `--resume-marker`, else `self` — and (b) the\n            // `current_dir()` the harness landed in. The int test declares a\n            // `[session.self]` with a SELF marker path and a `[session.resume]` with\n            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a\n            // `--resume` bringup proves both the template SELECTION and the cwd\n            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.\n            if let Some(marker) = arg(&args, \"--marker-file\") {\n                let which = if args.iter().any(|a| a == \"--resume-marker\") {\n                    \"resume\"\n                } else {\n                    \"self\"\n                };\n                let cwd = std::env::current_dir()\n                    .map(|p| p.display().to_string())\n                    .unwrap_or_else(|_| \"<no-cwd>\".to_string());\n                // `template=<self|resume>\\ncwd=<dir>\\n` — a parseable two-line proof.\n                let _ = std::fs::write(&marker, format!(\"template={which}\\ncwd={cwd}\\n\"));\n            }\n\n            println!(\"DUMMY_HARNESS_READY:{id}\");\n            let _ = std::io::stdout().flush();\n\n            // Bind the perch live (spt-hosted direct bind, mirroring the default\n            // stand-in below) — UNBOUND skeleton → online. SKIPPED in hold-unbound\n            // (the perch stays UNBOUND, the session stays live). A failure is fatal —\n            // the fixture must come up.\n            if !no_bind {\n                let mut bind_args = vec![\n                    \"api\".to_string(),\n                    \"--adapter\".to_string(),\n                    adapter,\n                    \"bind\".to_string(),\n                    id.clone(),\n                    \"--set-session-id\".to_string(),\n                    session_id,\n                ];\n                if let Some(ty) = &endpoint_type {\n                    bind_args.push(\"--type\".to_string());\n                    bind_args.push(ty.clone());\n                }\n                let bind = Command::new(&spt).args(&bind_args).status();\n                if !matches!(bind, Ok(s) if s.success()) {\n                    eprintln!(\"mock-session --mode {mode}: api bind failed: {bind:?}\");\n                    std::process::exit(1);\n                }\n            }\n\n            // Heartbeat forever — only an external kill (tab-close job reap, daemon\n            // stop, or an explicit kill) ends it.\n            let mut n: u64 = 0;\n            loop {\n                println!(\"DUMMY_HARNESS_TICK:{id}:{n}\");\n                let _ = std::io::stdout().flush();\n                n += 1;\n                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));\n            }\n        }\n        _ => {} // default: the agent-session stand-in below.\n    }\n\n    let id = arg(&args, \"--id\").unwrap_or_else(|| {\n        eprintln!(\"mock-session: --id required\");\n        std::process::exit(2);\n    });\n    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n    let spt = arg(&args, \"--spt\")\n        .or_else(|| std::env::var(\"SPT_BIN\").ok())\n        .unwrap_or_else(|| \"spt\".to_string());\n    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.\n    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.\n    let endpoint_type = arg(&args, \"--type\");\n    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n\n    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\n    let mut bind_args = vec![\n        \"api\".to_string(),\n…\n    ];\n…\n}\n\n[Showing lines 1-153 of 191. Use :154 to continue]"}],"details":{"truncation":{"content":"//! `mock-session` — the generic mock adapter's \"agent session\". A trivial,\r\n//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,\r\n//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's\r\n//! `[session.self]` template, it does what a real session's boot hook would do:\r\n//! fire `spt api bind` to flip its perch live, then `spt api state idle`.\r\n//!\r\n//! It shells out to the `spt` binary exactly as a real adapter would — resolved\r\n//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves\r\n//! the inbound contract is harness-agnostic: a dumb helper drives it with only\r\n//! `spt api` calls.\r\n\r\nuse std::process::Command;\r\n\r\nfn arg(args: &[String], flag: &str) -> Option<String> {\r\n    args.iter()\r\n        .position(|a| a == flag)\r\n        .and_then(|i| args.get(i + 1).cloned())\r\n}\r\n\r\nfn main() {\r\n    let args: Vec<String> = std::env::args().skip(1).collect();\r\n\r\n    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core\r\n    // binds its nested perch; the child just needs to launch). `echo` is the\r\n    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.\r\n    match arg(&args, \"--mode\").as_deref() {\r\n        Some(\"psyche\") => {\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| \"mock-psyche\".to_string());\r\n            println!(\"MOCK_PSYCHE_UP:{id}\");\r\n            return;\r\n        }\r\n        Some(\"echo\") => {\r\n            use std::io::Read;\r\n            let mut history = String::new();\r\n            let _ = std::io::stdin().read_to_string(&mut history);\r\n            // A deterministic brief; a real echo summarizes the history it read.\r\n            println!(\"mock echo brief ({} history bytes)\", history.len());\r\n            return;\r\n        }\r\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\r\n            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived\r\n            // stand-in for a real harness session. Binds its perch (the harness\r\n            // contract) exactly like the default stand-in, then prints a stdout\r\n            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL\r\n            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`\r\n            // attach path can be exercised end-to-end (the default mode exits\r\n            // immediately, which is why the v0.12.0 mock tests never caught the\r\n            // daemon+broker+PTY lifecycle defects). Isolates \"broker wedged /\r\n            // endpoint died\" from \"a real harness failed to launch.\"\r\n            //\r\n            // `--mode hold-unbound` (W6, REQ-ENDPOINT-UNBOUND-ATTACH): identical, but\r\n            // it NEVER binds — the broker session is live (PTY + heartbeat) while the\r\n            // perch stays the endpoint-run UNBOUND skeleton forever. That gives the\r\n            // int a deterministic, race-free live pre-bind session to prove the\r\n            // attach-before-bind contract (`spt rc` attaches a STATUS_UNBOUND\r\n            // endpoint) + the real picker Unbound render against a REAL session. The\r\n            // UNBOUND→ONLINE flip is proven separately by the normal `dummy` bringup.\r\n            let no_bind = mode == \"hold-unbound\";\r\n            use std::io::Write;\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n                eprintln!(\"mock-session --mode dummy: --id required\");\r\n                std::process::exit(2);\r\n            });\r\n            let session_id =\r\n                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n            let spt = arg(&args, \"--spt\")\r\n                .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n                .unwrap_or_else(|| \"spt\".to_string());\r\n            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n            let endpoint_type = arg(&args, \"--type\");\r\n            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\r\n                .and_then(|s| s.parse().ok())\r\n                .unwrap_or(500);\r\n\r\n            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when\r\n            // `--marker-file <path>` is set, write a marker whose CONTENTS record\r\n            // (a) which session template launched this dummy — `resume` if the\r\n            // command carried `--resume-marker`, else `self` — and (b) the\r\n            // `current_dir()` the harness landed in. The int test declares a\r\n            // `[session.self]` with a SELF marker path and a `[session.resume]` with\r\n            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a\r\n            // `--resume` bringup proves both the template SELECTION and the cwd\r\n            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.\r\n            if let Some(marker) = arg(&args, \"--marker-file\") {\r\n                let which = if args.iter().any(|a| a == \"--resume-marker\") {\r\n                    \"resume\"\r\n                } else {\r\n                    \"self\"\r\n                };\r\n                let cwd = std::env::current_dir()\r\n                    .map(|p| p.display().to_string())\r\n                    .unwrap_or_else(|_| \"<no-cwd>\".to_string());\r\n                // `template=<self|resume>\\ncwd=<dir>\\n` — a parseable two-line proof.\r\n                let _ = std::fs::write(&marker, format!(\"template={which}\\ncwd={cwd}\\n\"));\r\n            }\r\n\r\n            println!(\"DUMMY_HARNESS_READY:{id}\");\r\n            let _ = std::io::stdout().flush();\r\n\r\n            // Bind the perch live (spt-hosted direct bind, mirroring the default\r\n            // stand-in below) — UNBOUND skeleton → online. SKIPPED in hold-unbound\r\n            // (the perch stays UNBOUND, the session stays live). A failure is fatal —\r\n            // the fixture must come up.\r\n            if !no_bind {\r\n                let mut bind_args = vec![\r\n                    \"api\".to_string(),\r\n                    \"--adapter\".to_string(),\r\n                    adapter,\r\n                    \"bind\".to_string(),\r\n                    id.clone(),\r\n                    \"--set-session-id\".to_string(),\r\n                    session_id,\r\n                ];\r\n                if let Some(ty) = &endpoint_type {\r\n                    bind_args.push(\"--type\".to_string());\r\n                    bind_args.push(ty.clone());\r\n                }\r\n                let bind = Command::new(&spt).args(&bind_args).status();\r\n                if !matches!(bind, Ok(s) if s.success()) {\r\n                    eprintln!(\"mock-session --mode {mode}: api bind failed: {bind:?}\");\r\n                    std::process::exit(1);\r\n                }\r\n            }\r\n\r\n            // Heartbeat forever — only an external kill (tab-close job reap, daemon\r\n            // stop, or an explicit kill) ends it.\r\n            let mut n: u64 = 0;\r\n            loop {\r\n                println!(\"DUMMY_HARNESS_TICK:{id}:{n}\");\r\n                let _ = std::io::stdout().flush();\r\n                n += 1;\r\n                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));\r\n            }\r\n        }\r\n        _ => {} // default: the agent-session stand-in below.\r\n    }\r\n\r\n    let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n        eprintln!(\"mock-session: --id required\");\r\n        std::process::exit(2);\r\n    });\r\n    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n    let spt = arg(&args, \"--spt\")\r\n        .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n        .unwrap_or_else(|| \"spt\".to_string());\r\n    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.\r\n    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.\r\n    let endpoint_type = arg(&args, \"--type\");\r\n    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n\r\n    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\r\n    let mut bind_args = vec![\r\n        \"api\".to_string(),\r","truncated":true,"truncatedBy":"lines","totalLines":191,"totalBytes":8023,"outputLines":153,"outputBytes":8023,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! `mock-session` — the generic mock adapter's \"agent session\". A trivial,\r\n//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,\r\n//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's\r\n//! `[session.self]` template, it does what a real session's boot hook would do:\r\n//! fire `spt api bind` to flip its perch live, then `spt api state idle`.\r\n//!\r\n//! It shells out to the `spt` binary exactly as a real adapter would — resolved\r\n//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves\r\n//! the inbound contract is harness-agnostic: a dumb helper drives it with only\r\n//! `spt api` calls.\r\n\r\nuse std::process::Command;\r\n\r\nfn arg(args: &[String], flag: &str) -> Option<String> {\r\n    args.iter()\r\n        .position(|a| a == flag)\r\n        .and_then(|i| args.get(i + 1).cloned())\r\n}\r\n\r\nfn main() {\r\n    let args: Vec<String> = std::env::args().skip(1).collect();\r\n\r\n    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core\r\n    // binds its nested perch; the child just needs to launch). `echo` is the\r\n    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.\r\n    match arg(&args, \"--mode\").as_deref() {\r\n        Some(\"psyche\") => {\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| \"mock-psyche\".to_string());\r\n            println!(\"MOCK_PSYCHE_UP:{id}\");\r\n            return;\r\n        }\r\n        Some(\"echo\") => {\r\n            use std::io::Read;\r\n            let mut history = String::new();\r\n            let _ = std::io::stdin().read_to_string(&mut history);\r\n            // A deterministic brief; a real echo summarizes the history it read.\r\n            println!(\"mock echo brief ({} history bytes)\", history.len());\r\n            return;\r\n        }\r\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\r\n            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived\r\n            // stand-in for a real harness session. Binds its perch (the harness\r\n            // contract) exactly like the default stand-in, then prints a stdout\r\n            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL\r\n            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`\r\n            // attach path can be exercised end-to-end (the default mode exits\r\n            // immediately, which is why the v0.12.0 mock tests never caught the\r\n            // daemon+broker+PTY lifecycle defects). Isolates \"broker wedged /\r\n            // endpoint died\" from \"a real harness failed to launch.\"\r\n            //\r\n            // `--mode hold-unbound` (W6, REQ-ENDPOINT-UNBOUND-ATTACH): identical, but\r\n            // it NEVER binds — the broker session is live (PTY + heartbeat) while the\r\n            // perch stays the endpoint-run UNBOUND skeleton forever. That gives the\r\n            // int a deterministic, race-free live pre-bind session to prove the\r\n            // attach-before-bind contract (`spt rc` attaches a STATUS_UNBOUND\r\n            // endpoint) + the real picker Unbound render against a REAL session. The\r\n            // UNBOUND→ONLINE flip is proven separately by the normal `dummy` bringup.\r\n            let no_bind = mode == \"hold-unbound\";\r\n            use std::io::Write;\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n                eprintln!(\"mock-session --mode dummy: --id required\");\r\n                std::process::exit(2);\r\n            });\r\n            let session_id =\r\n                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n            let spt = arg(&args, \"--spt\")\r\n                .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n                .unwrap_or_else(|| \"spt\".to_string());\r\n            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n            let endpoint_type = arg(&args, \"--type\");\r\n            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\r\n                .and_then(|s| s.parse().ok())\r\n                .unwrap_or(500);\r\n\r\n            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when\r\n            // `--marker-file <path>` is set, write a marker whose CONTENTS record\r\n            // (a) which session template launched this dummy — `resume` if the\r\n            // command carried `--resume-marker`, else `self` — and (b) the\r\n            // `current_dir()` the harness landed in. The int test declares a\r\n            // `[session.self]` with a SELF marker path and a `[session.resume]` with\r\n            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a\r\n            // `--resume` bringup proves both the template SELECTION and the cwd\r\n            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.\r\n            if let Some(marker) = arg(&args, \"--marker-file\") {\r\n                let which = if args.iter().any(|a| a == \"--resume-marker\") {\r\n                    \"resume\"\r\n                } else {\r\n                    \"self\"\r\n                };\r\n                let cwd = std::env::current_dir()\r\n                    .map(|p| p.display().to_string())\r\n                    .unwrap_or_else(|_| \"<no-cwd>\".to_string());\r\n                // `template=<self|resume>\\ncwd=<dir>\\n` — a parseable two-line proof.\r\n                let _ = std::fs::write(&marker, format!(\"template={which}\\ncwd={cwd}\\n\"));\r\n            }\r\n\r\n            println!(\"DUMMY_HARNESS_READY:{id}\");\r\n            let _ = std::io::stdout().flush();\r\n\r\n            // Bind the perch live (spt-hosted direct bind, mirroring the default\r\n            // stand-in below) — UNBOUND skeleton → online. SKIPPED in hold-unbound\r\n            // (the perch stays UNBOUND, the session stays live). A failure is fatal —\r\n            // the fixture must come up.\r\n            if !no_bind {\r\n                let mut bind_args = vec![\r\n                    \"api\".to_string(),\r\n                    \"--adapter\".to_string(),\r\n                    adapter,\r\n                    \"bind\".to_string(),\r\n                    id.clone(),\r\n                    \"--set-session-id\".to_string(),\r\n                    session_id,\r\n                ];\r\n                if let Some(ty) = &endpoint_type {\r\n                    bind_args.push(\"--type\".to_string());\r\n                    bind_args.push(ty.clone());\r\n                }\r\n                let bind = Command::new(&spt).args(&bind_args).status();\r\n                if !matches!(bind, Ok(s) if s.success()) {\r\n                    eprintln!(\"mock-session --mode {mode}: api bind failed: {bind:?}\");\r\n                    std::process::exit(1);\r\n                }\r\n            }\r\n\r\n            // Heartbeat forever — only an external kill (tab-close job reap, daemon\r\n            // stop, or an explicit kill) ends it.\r\n            let mut n: u64 = 0;\r\n            loop {\r\n                println!(\"DUMMY_HARNESS_TICK:{id}:{n}\");\r\n                let _ = std::io::stdout().flush();\r\n                n += 1;\r\n                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));\r\n            }\r\n        }\r\n        _ => {} // default: the agent-session stand-in below.\r\n    }\r\n\r\n    let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n        eprintln!(\"mock-session: --id required\");\r\n        std::process::exit(2);\r\n    });\r\n    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n    let spt = arg(&args, \"--spt\")\r\n        .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n        .unwrap_or_else(|| \"spt\".to_string());\r\n    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.\r\n    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.\r\n    let endpoint_type = arg(&args, \"--type\");\r\n    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n\r\n    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\r\n    let mut bind_args = vec![\r\n        \"api\".to_string(),\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,null,160,null,190]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\adapters\\mock\\src\\main.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":191,"totalBytes":8023,"outputLines":153,"outputBytes":8023,"shownRange":{"start":1,"end":153},"nextOffset":154}}},"isError":false,"timestamp":1784195843535}}
{"type":"message","id":"841395ce","parentId":"081954b4","timestamp":"2026-07-16T09:57:23.540Z","message":{"role":"toolResult","toolCallId":"call_HygIkHxgA3kUrpDw3xXwwP85|fc_0b42584a8ab0545d016a58ab037940819995c5c665bf14799c","toolName":"read","content":[{"type":"text","text":"fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n…\n        args: vec![\n…\n                .to_string(),\n        ],\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 `KIND_SESSIONS` round-trip, returning the full reply. The dispatch runs\n/// `reap_dead_controller` → the time-based `stall_evict_controller`, so polling\n/// this both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n/// Bounded thread + channel (a wedge surfaces as a timeout, not a hang), like the\n/// existing [`stall_evict_count`] helper. The transient conn is dropped when the\n/// thread returns.\n#[cfg(windows)]\nfn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {\n    let name = name.to_string();\n    let (tx, rx) = std::sync::mpsc::channel();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(&mut c, spt_daemon::msg::KIND_SESSIONS, serde_json::Value::Null);\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == spt_daemon::msg::KIND_SESSIONS_REPLY => {\n                    let r: spt_daemon::msg::SessionsReply =\n                        serde_json::from_value(f.payload).unwrap();\n                    let _ = tx.send(r);\n                    return;\n                }\n                Ok(_) => continue,\n                Err(_) => return,\n            }\n        }\n    });\n    rx.recv_timeout(deadline).ok()\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    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))\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)\n                            .map(|s| s.resume_seq)\n                    })\n                });\n            break;\n        }\n        thread::sleep(Duration::from_millis(150));\n    }\n\n    // ── Step 7: WITHOUT dropping R's Stream, poll conn_handler_count() until it reads\n    //    ZERO (a zero sample tolerates a transient query conn briefly bumping it),\n    //    bounded at t_release + 2 s. PRE-FIX this stays ≥1 forever (the black-holed\n    //    conn's handler never exits until the client drops) = the RED. ──\n    let step7_deadline =\n        t_release.unwrap_or_else(Instant::now) + Duration::from_secs(2);\n    let mut handler_released = false;\n    while Instant::now() < step7_deadline {\n        if broker.conn_handler_count() == 0 {\n            handler_released = true;\n            break;\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n\n    // ── Step 8: attempt a write on the OLD client Stream. Pre-fix it succeeds (the\n    //    conn is physically alive); post-fix it FAILS (the conn was retired). ──\n    let old_client_write: Option<std::io::Result<()>> = r_stream.as_mut().map(|s| {\n        write_frame(\n            s,\n            &Envelope::new(spt_daemon::msg::KIND_SESSIONS, serde_json::Value::Null),\n        )\n    });\n    let old_client_write_is_err = old_client_write.as_ref().map(|r| r.is_err());\n\n    // ── Step 9: fresh controller from the frozen cursor — assert the first replayed\n    //    frame's seq equals that cursor (no lost frame). Bounded thread + channel. ──\n    let resume_seq = frozen_cursor.unwrap_or(0);\n    let s9_name = name.clone();\n    let (s9_tx, s9_rx) =\n        std::sync::mpsc::channel::<(Option<SubscribeOutcome>, Option<u64>)>();\n    thread::spawn(move || {\n        let mut c = connect(&s9_name);\n        send(\n            &mut c,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: resume_seq,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-two\".to_string()),\n            })\n            .unwrap(),\n        );\n        // Single loop: capture the outcome (on SUBSCRIBED) AND the first replayed\n        // seq (on the FIRST KIND_OUTPUT) — whichever arrives first. The writer thread\n        // spawned by resolve_subscribe races the inline SUBSCRIBED reply for the same\n        // send lock, so a replay frame can legitimately hit the wire BEFORE SUBSCRIBED;\n        // discarding output while waiting for SUBSCRIBED would drop the true first\n        // replayed frame. Break once BOTH are known.\n        let deadline = Instant::now() + Duration::from_secs(15);\n        let mut outcome: Option<SubscribeOutcome> = None;\n        let mut first_seq: Option<u64> = None;\n        while Instant::now() < deadline && (outcome.is_none() || first_seq.is_none()) {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => {\n                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\n                    outcome = Some(reply.outcome);\n                }\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\n                    if first_seq.is_none() {\n                        let ev: spt_daemon::msg::OutputEvent =\n                            serde_json::from_value(f.payload).unwrap();\n                        first_seq = Some(ev.seq);\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        let _ = s9_tx.send((outcome, first_seq));\n    });\n    let (s9_outcome, s9_first_seq) = s9_rx\n        .recv_timeout(Duration::from_secs(18))\n        .unwrap_or((None, None));\n\n    // ── Teardown BEFORE asserting (existing test's pattern): kill the PTY child and\n    //    stop the broker so a failing assert leaves no orphan child. Drop the\n    //    black-holed Stream only now (the very end). ──\n    if let Some(pid) = broker.session_pid(sid) {\n        kill_pid(pid);\n    }\n    broker.stop();\n    drop(r_stream);\n\n    eprintln!(\n        \"=== UWEDGE-R4 GATE: saw_seed={saw_seed} seed_high={seed_high} \\\n         max_drained_seq={max_drained_seq} burst_advanced={burst_advanced} \\\n         stall_evicts_seen={stall_evicts_seen} handler_released={handler_released} \\\n         old_client_write_is_err={old_client_write_is_err:?} resume_seq={resume_seq} \\\n         s9_outcome={s9_outcome:?} s9_first_seq={s9_first_seq:?} ===\"\n    );\n\n    // Harness precondition: the black-holed controller must have handed its stream\n    // back (else the whole physical-retirement observation is vacuous). Distinct\n    // from the product asserts below.\n    assert!(\n        old_client_write_is_err.is_some(),\n        \"HARNESS: the black-holed controller never handed its stream back — the \\\n         PowerShell child likely never produced SEED (saw_seed={saw_seed}). This is a \\\n         test-environment failure, not the product RED.\"\n    );\n\n    // The burst is real: the ring high-water advanced ≥64 frames past the trigger.\n    assert!(\n        burst_advanced,\n        \"the PTY/ring high-water must advance ≥64 frames past the burst trigger \\\n         (seed_high={seed_high}) — the finite burst must actually flow into the ring\"\n    );\n    // A real seq>0 controller frame was delivered (the load-gating precondition for\n    // the wedge — seq-0 boot conns alone never reproduce it).\n    assert!(\n        max_drained_seq > 0,\n        \"the controller must have drained a real seq>0 frame before black-holing \\\n         (load-gated repro); got max_drained_seq={max_drained_seq}\"\n    );\n    // ONE wedged controller, one release — observed as one tally, or two when the\n    // physical poison races the logical reap (poison-beats-evict, see step 6).\n    // Three or more would be a genuine second eviction: still a failure.\n    assert!(\n        (1..=2).contains(&stall_evicts_seen),\n        \"the one wedged controller's release must be tallied once (logical reap OR \\\n         physical poison) or twice (both raced — poison-beats-evict, field-accepted \\\n         v0.30.6); got {stall_evicts_seen}\"\n    );\n    // ── THE RED (pre-fix): logical eviction freed the ROLE, but the black-holed\n    //    conn's handle_conn thread must PHYSICALLY finish within the write bound of\n    //    logical release — WITHOUT the client dropping its end. Pre-fix the detached\n    //    writer stays wedged in write_frame and the reader never EOFs, so the handler\n    //    count never falls to 0. ──\n    assert!(\n        handler_released,\n        \"the black-holed connection's handle_conn thread must finish (conn_handler_count \\\n         → 0) within 2 s of logical release, WITHOUT the client dropping its end — the \\\n         bounded/cancelable write must poison+close the physical conn. PRE-FIX RED: the \\\n         detached writer stays wedged in write_frame and the handler never exits.\"\n    );\n    // ── THE RED (pre-fix): a write on the old client conn must FAIL — the physical\n    //    connection was retired. Pre-fix the conn is still alive so the write succeeds. ──\n    assert_eq!(\n        old_client_write_is_err,\n        Some(true),\n        \"a write on the OLD client connection must FAIL once the physical conn is \\\n         retired. PRE-FIX RED: the conn is still alive so the write succeeds (Ok).\"\n    );\n    // The fresh controller resumes from the frozen cursor with NO lost frame.\n    assert!(\n        matches!(\n            s9_outcome,\n            Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)\n        ),\n        \"a fresh controller from the frozen cursor must take control; got {s9_outcome:?}\"\n    );\n    assert_eq!(\n        s9_first_seq,\n        Some(resume_seq),\n        \"the fresh controller's FIRST replayed frame seq must equal the frozen resume \\\n         cursor (no lost frame); got first_seq={s9_first_seq:?} resume_seq={resume_seq}\"\n    );\n}\n"}],"details":{"displayContent":{"text":"fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    SpawnReq {\n…\n        args: vec![\n…\n                .to_string(),\r\n        ],\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/// One `KIND_SESSIONS` round-trip, returning the full reply. The dispatch runs\r\n/// `reap_dead_controller` → the time-based `stall_evict_controller`, so polling\r\n/// this both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\r\n/// Bounded thread + channel (a wedge surfaces as a timeout, not a hang), like the\r\n/// existing [`stall_evict_count`] helper. The transient conn is dropped when the\r\n/// thread returns.\r\n#[cfg(windows)]\r\nfn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {\r\n    let name = name.to_string();\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    thread::spawn(move || {\r\n        let mut c = connect(&name);\r\n        send(&mut c, spt_daemon::msg::KIND_SESSIONS, serde_json::Value::Null);\r\n        loop {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == spt_daemon::msg::KIND_SESSIONS_REPLY => {\r\n                    let r: spt_daemon::msg::SessionsReply =\r\n                        serde_json::from_value(f.payload).unwrap();\r\n                    let _ = tx.send(r);\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => return,\r\n            }\r\n        }\r\n    });\r\n    rx.recv_timeout(deadline).ok()\r\n}\r\n\r\n// [int->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n#[cfg(windows)]\r\n#[test]\r\nfn non_draining_controller_stall_evict_releases_writer_and_connection() {\r\n    init_home();\r\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\r\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\r\n\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    // ── Step 2: spawn the real SEED+burst PTY child from conn A, then DROP A so the\r\n    //    ONLY persistent brain conn later is the black-holed controller R. ──\r\n    let mut a = connect(&name);\r\n    send(\r\n        &mut a,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(seed_burst_spawn_req(\"uwedge-r4-ep\")).unwrap(),\r\n    );\r\n    let sid = loop {\r\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\r\n        if f.kind == KIND_SPAWNED {\r\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\r\n        }\r\n    };\r\n    drop(a);\r\n\r\n    // ── Steps 3+4: remote controller R. On a dedicated thread it subscribes Control\r\n    //    from_seq 0, drains output until it has seen `SEED` (proving a real controller\r\n    //    frame flowed), signals main so main can freeze the PRE-burst high-water, then\r\n    //    on main's go it fires the burst trigger and drains a few burst frames (a real\r\n    //    seq>0 frame delivered to the controller) and STOPS reading. It hands the\r\n    //    Stream back to main UNREAD — main holds it, never draining, so the broker's\r\n    //    R-writer parks in `write_frame` exactly as against a suspended brain. R's\r\n    //    Stream is NEVER dropped until the very end of the test. ──\r\n    let r_name = name.clone();\r\n    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\r\n    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\r\n    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\r\n    thread::spawn(move || {\r\n        let mut r = connect(&r_name);\r\n        send(\r\n            &mut r,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Control,\r\n                by: Some(\"operator-one\".to_string()),\r\n            })\r\n            .unwrap(),\r\n        );\r\n        // Read until SUBSCRIBED.\r\n        loop {\r\n            match read_frame(&mut r) {\r\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\r\n                Ok(_) => continue,\r\n                Err(_) => {\r\n                    let _ = seed_tx.send((false, 0));\r\n                    return;\r\n                }\r\n            }\r\n        }\r\n        // Drain live output until the concatenated decoded bytes contain `SEED`\r\n        // (search the CONCATENATED stream — ConPTY echoes input and emits VT noise,\r\n        // so per-frame matching is unreliable). Track the max seq seen.\r\n        let mut acc: Vec<u8> = Vec::new();\r\n        let mut max_seq: u64 = 0;\r\n        let mut saw_seed = false;\r\n        let seed_deadline = Instant::now() + Duration::from_secs(30);\r\n        while Instant::now() < seed_deadline {\r\n            match read_frame(&mut r) {\r\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\r\n                    let ev: spt_daemon::msg::OutputEvent =\r\n                        serde_json::from_value(f.payload).unwrap();\r\n                    max_seq = max_seq.max(ev.seq);\r\n                    if let Ok(bytes) = spt_daemon::msg::decode_bytes(&ev.data_b64) {\r\n                        acc.extend_from_slice(&bytes);\r\n                    }\r\n                    if acc.windows(4).any(|w| w == b\"SEED\") {\r\n                        saw_seed = true;\r\n                        break;\r\n                    }\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        // Signal SEED reached (PRE-trigger: the child is parked on the first\r\n        // Read-Host, so main can freeze the pre-burst high-water now).\r\n        let _ = seed_tx.send((saw_seed, max_seq));\r\n        // Wait for main's go, then fire the burst trigger. `\\r\\n` satisfies the first\r\n        // Read-Host; ack=false (the rc/operator fire-and-forward path).\r\n        let _ = go_rx.recv_timeout(Duration::from_secs(15));\r\n        send(\r\n            &mut r,\r\n            spt_daemon::msg::KIND_INPUT,\r\n            serde_json::to_value(spt_daemon::msg::InputReq {\r\n                session_id: sid,\r\n                data_b64: spt_daemon::msg::encode_bytes(b\"\\r\\n\"),\r\n                op_id: None,\r\n                minter: None,\r\n                ack: false,\r\n            })\r\n            .unwrap(),\r\n        );\r\n        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame is\r\n        // proven delivered, then STOP reading forever (the black-hole). The remaining\r\n        // ~1300 frames flood the now-unread socket → the R-writer wedges in write_frame.\r\n        let post_deadline = Instant::now() + Duration::from_secs(5);\r\n        let mut post = 0u32;\r\n        while (max_seq == 0 || post < 8) && Instant::now() < post_deadline {\r\n            match read_frame(&mut r) {\r\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\r\n                    let ev: spt_daemon::msg::OutputEvent =\r\n                        serde_json::from_value(f.payload).unwrap();\r\n                    max_seq = max_seq.max(ev.seq);\r\n                    post += 1;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        // Hand the Stream back to main UNREAD (main holds it = the black-holed client).\r\n        let _ = hand_tx.send((r, max_seq));\r\n        // Thread exits; the broker's R-writer stays wedged on the remaining burst.\r\n    });\r\n\r\n    // Main: recv SEED-reached, then freeze the PRE-burst high-water (the trigger has\r\n    // NOT fired — R is parked on go_rx).\r\n    let (saw_seed, _seed_max) = seed_rx\r\n        .recv_timeout(Duration::from_secs(35))\r\n        .unwrap_or((false, 0));\r\n    let seed_high = broker.session_output_seq(sid).unwrap_or(0);\r\n    let _ = go_tx.send(());\r\n    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\r\n    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\r\n    let mut r_stream: Option<Stream> = None;\r\n    let mut max_drained_seq: u64 = 0;\r\n    if let Some((stream, mds)) = handed {\r\n        r_stream = Some(stream);\r\n        max_drained_seq = mds;\r\n    }\r\n\r\n    // ── Step 5: prove the PTY/ring high-water advances ≥ 64 frames past the trigger\r\n    //    (the burst is real and flowing into the ring, independent of the wedged\r\n    //    controller). ──\r\n    let mut burst_advanced = false;\r\n    let step5_deadline = Instant::now() + Duration::from_secs(20);\r\n    while Instant::now() < step5_deadline {\r\n        if let Some(hw) = broker.session_output_seq(sid) {\r\n            if hw >= seed_high + 64 {\r\n                burst_advanced = true;\r\n                break;\r\n            }\r\n        }\r\n        thread::sleep(Duration::from_millis(100));\r\n    }\r\n\r\n    // ── Step 6: exactly ONE logical stall eviction, observed via an INDEPENDENT\r\n    //    DRAINING control-plane connection (the KIND_SESSIONS reap runs the 400 ms\r\n    //    stall-evict). Record t_release at first observation and freeze the resume\r\n    //    cursor for the sid row. ──\r\n    // POISON-BEATS-EVICT (F-039 rider, doyle-ruled 2026-07-10, 3rd CI trip): the\r\n    // ONE wedged controller's release can be tallied by the LOGICAL reap\r\n    // (stall_evict_controller), by the PHYSICAL bound (the conn watchdog poisons\r\n    // first and note_controller_write_retired records the same event class —\r\n    // the field-accepted v0.30.6 behavior), or by BOTH when the watchdog races\r\n    // the reap's blocked-window sample. So the tally can step 0→2 and an\r\n    // `== 1` break-condition misses it forever (the evicts_seen=0 flake). The\r\n    // loop breaks on ≥1; the assert below accepts {1, 2} — anything more is a\r\n    // real second eviction and still fails.\r\n    let mut stall_evicts_seen: u64 = 0;\r\n    let mut t_release: Option<Instant> = None;\r\n    let mut frozen_cursor: Option<u64> = None;\r\n    let step6_deadline = Instant::now() + Duration::from_secs(10);\r\n    while Instant::now() < step6_deadline {\r\n        let sessions = query_sessions(&name, Duration::from_secs(3));\r\n        let count = stall_evict_count(&name, Duration::from_secs(3));\r\n        if count >= 1 {\r\n            stall_evicts_seen = count;\r\n            t_release = Some(Instant::now());\r\n            frozen_cursor = sessions\r\n                .as_ref()\r\n                .and_then(|r| r.sessions.iter().find(|s| s.session_id == sid))\r\n                .map(|s| s.resume_seq)\r\n                .or_else(|| {\r\n                    query_sessions(&name, Duration::from_secs(3)).and_then(|r| {\r\n                        r.sessions\r\n                            .iter()\r\n                            .find(|s| s.session_id == sid)\r\n                            .map(|s| s.resume_seq)\r\n                    })\r\n                });\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(150));\r\n    }\r\n\r\n    // ── Step 7: WITHOUT dropping R's Stream, poll conn_handler_count() until it reads\r\n    //    ZERO (a zero sample tolerates a transient query conn briefly bumping it),\r\n    //    bounded at t_release + 2 s. PRE-FIX this stays ≥1 forever (the black-holed\r\n    //    conn's handler never exits until the client drops) = the RED. ──\r\n    let step7_deadline =\r\n        t_release.unwrap_or_else(Instant::now) + Duration::from_secs(2);\r\n    let mut handler_released = false;\r\n    while Instant::now() < step7_deadline {\r\n        if broker.conn_handler_count() == 0 {\r\n            handler_released = true;\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n\r\n    // ── Step 8: attempt a write on the OLD client Stream. Pre-fix it succeeds (the\r\n    //    conn is physically alive); post-fix it FAILS (the conn was retired). ──\r\n    let old_client_write: Option<std::io::Result<()>> = r_stream.as_mut().map(|s| {\r\n        write_frame(\r\n            s,\r\n            &Envelope::new(spt_daemon::msg::KIND_SESSIONS, serde_json::Value::Null),\r\n        )\r\n    });\r\n    let old_client_write_is_err = old_client_write.as_ref().map(|r| r.is_err());\r\n\r\n    // ── Step 9: fresh controller from the frozen cursor — assert the first replayed\r\n    //    frame's seq equals that cursor (no lost frame). Bounded thread + channel. ──\r\n    let resume_seq = frozen_cursor.unwrap_or(0);\r\n    let s9_name = name.clone();\r\n    let (s9_tx, s9_rx) =\r\n        std::sync::mpsc::channel::<(Option<SubscribeOutcome>, Option<u64>)>();\r\n    thread::spawn(move || {\r\n        let mut c = connect(&s9_name);\r\n        send(\r\n            &mut c,\r\n            KIND_SUBSCRIBE,\r\n            serde_json::to_value(SubscribeReq {\r\n                session_id: sid,\r\n                from_seq: resume_seq,\r\n                intent: AttachIntent::Control,\r\n                by: Some(\"operator-two\".to_string()),\r\n            })\r\n            .unwrap(),\r\n        );\r\n        // Single loop: capture the outcome (on SUBSCRIBED) AND the first replayed\r\n        // seq (on the FIRST KIND_OUTPUT) — whichever arrives first. The writer thread\r\n        // spawned by resolve_subscribe races the inline SUBSCRIBED reply for the same\r\n        // send lock, so a replay frame can legitimately hit the wire BEFORE SUBSCRIBED;\r\n        // discarding output while waiting for SUBSCRIBED would drop the true first\r\n        // replayed frame. Break once BOTH are known.\r\n        let deadline = Instant::now() + Duration::from_secs(15);\r\n        let mut outcome: Option<SubscribeOutcome> = None;\r\n        let mut first_seq: Option<u64> = None;\r\n        while Instant::now() < deadline && (outcome.is_none() || first_seq.is_none()) {\r\n            match read_frame(&mut c) {\r\n                Ok(f) if f.kind == KIND_SUBSCRIBED => {\r\n                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\r\n                    outcome = Some(reply.outcome);\r\n                }\r\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\r\n                    if first_seq.is_none() {\r\n                        let ev: spt_daemon::msg::OutputEvent =\r\n                            serde_json::from_value(f.payload).unwrap();\r\n                        first_seq = Some(ev.seq);\r\n                    }\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        let _ = s9_tx.send((outcome, first_seq));\r\n    });\r\n    let (s9_outcome, s9_first_seq) = s9_rx\r\n        .recv_timeout(Duration::from_secs(18))\r\n        .unwrap_or((None, None));\r\n\r\n    // ── Teardown BEFORE asserting (existing test's pattern): kill the PTY child and\r\n    //    stop the broker so a failing assert leaves no orphan child. Drop the\r\n    //    black-holed Stream only now (the very end). ──\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n    broker.stop();\r\n    drop(r_stream);\r\n\r\n    eprintln!(\r\n        \"=== UWEDGE-R4 GATE: saw_seed={saw_seed} seed_high={seed_high} \\\r\n         max_drained_seq={max_drained_seq} burst_advanced={burst_advanced} \\\r\n         stall_evicts_seen={stall_evicts_seen} handler_released={handler_released} \\\r\n         old_client_write_is_err={old_client_write_is_err:?} resume_seq={resume_seq} \\\r\n         s9_outcome={s9_outcome:?} s9_first_seq={s9_first_seq:?} ===\"\r\n    );\r\n\r\n    // Harness precondition: the black-holed controller must have handed its stream\r\n    // back (else the whole physical-retirement observation is vacuous). Distinct\r\n    // from the product asserts below.\r\n    assert!(\r\n        old_client_write_is_err.is_some(),\r\n        \"HARNESS: the black-holed controller never handed its stream back — the \\\r\n         PowerShell child likely never produced SEED (saw_seed={saw_seed}). This is a \\\r\n         test-environment failure, not the product RED.\"\r\n    );\r\n\r\n    // The burst is real: the ring high-water advanced ≥64 frames past the trigger.\r\n    assert!(\r\n        burst_advanced,\r\n        \"the PTY/ring high-water must advance ≥64 frames past the burst trigger \\\r\n         (seed_high={seed_high}) — the finite burst must actually flow into the ring\"\r\n    );\r\n    // A real seq>0 controller frame was delivered (the load-gating precondition for\r\n    // the wedge — seq-0 boot conns alone never reproduce it).\r\n    assert!(\r\n        max_drained_seq > 0,\r\n        \"the controller must have drained a real seq>0 frame before black-holing \\\r\n         (load-gated repro); got max_drained_seq={max_drained_seq}\"\r\n    );\r\n    // ONE wedged controller, one release — observed as one tally, or two when the\r\n    // physical poison races the logical reap (poison-beats-evict, see step 6).\r\n    // Three or more would be a genuine second eviction: still a failure.\r\n    assert!(\r\n        (1..=2).contains(&stall_evicts_seen),\r\n        \"the one wedged controller's release must be tallied once (logical reap OR \\\r\n         physical poison) or twice (both raced — poison-beats-evict, field-accepted \\\r\n         v0.30.6); got {stall_evicts_seen}\"\r\n    );\r\n    // ── THE RED (pre-fix): logical eviction freed the ROLE, but the black-holed\r\n    //    conn's handle_conn thread must PHYSICALLY finish within the write bound of\r\n    //    logical release — WITHOUT the client dropping its end. Pre-fix the detached\r\n    //    writer stays wedged in write_frame and the reader never EOFs, so the handler\r\n    //    count never falls to 0. ──\r\n    assert!(\r\n        handler_released,\r\n        \"the black-holed connection's handle_conn thread must finish (conn_handler_count \\\r\n         → 0) within 2 s of logical release, WITHOUT the client dropping its end — the \\\r\n         bounded/cancelable write must poison+close the physical conn. PRE-FIX RED: the \\\r\n         detached writer stays wedged in write_frame and the handler never exits.\"\r\n    );\r\n    // ── THE RED (pre-fix): a write on the old client conn must FAIL — the physical\r\n    //    connection was retired. Pre-fix the conn is still alive so the write succeeds. ──\r\n    assert_eq!(\r\n        old_client_write_is_err,\r\n        Some(true),\r\n        \"a write on the OLD client connection must FAIL once the physical conn is \\\r\n         retired. PRE-FIX RED: the conn is still alive so the write succeeds (Ok).\"\r\n    );\r\n    // The fresh controller resumes from the frozen cursor with NO lost frame.\r\n    assert!(\r\n        matches!(\r\n            s9_outcome,\r\n            Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)\r\n        ),\r\n        \"a fresh controller from the frozen cursor must take control; got {s9_outcome:?}\"\r\n    );\r\n    assert_eq!(\r\n        s9_first_seq,\r\n        Some(resume_seq),\r\n        \"the fresh controller's FIRST replayed frame seq must equal the frozen resume \\\r\n         cursor (no lost frame); got first_seq={s9_first_seq:?} resume_seq={resume_seq}\"\r\n    );\r\n}\r\n","startLine":421,"lineNumbers":[421,422,null,424,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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\brain_decouple.rs"}}},"isError":false,"timestamp":1784195843540}}
{"type":"message","id":"aa15c3cf","parentId":"841395ce","timestamp":"2026-07-16T09:57:23.561Z","message":{"role":"toolResult","toolCallId":"call_FLFlXuEqBszMtT42HiYPSZrG|fc_0b42584a8ab0545d016a58ab0379308199a184b99405f6b9e5","toolName":"read","content":[{"type":"text","text":"//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\n//! local terminal to a broker-held PTY session.\n//!\n//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\n//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\n//! degenerate single-node case: the operator dials its OWN node (loopback peer)\n//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\n//! against the same broker — the exact cross-node accept+serve path, no local\n//! special-case branch. The one-path invariant lives at this pump, not the\n//! transport under the stream.\n//!\n//! **Detach never kills the session.** The broker owns the PTY; closing our send\n//! side ends the viewport with the session still running (`serve_attach` →\n//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n//! attach-lifecycle invariant).\n//!\n//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\n//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\n//! (and watches for the detach keybind); the main loop interleaves draining that\n//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\n// [impl->REQ-RC-1]\n\nuse std::io::{Read, Write};\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::mpsc;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::brain::{now_ms, Brain, BrokerEvent};\nuse spt_daemon::msg::decode_bytes;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n\n/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n/// literal ctrl-b to the session; any other byte after the prefix is sent\n/// through verbatim (prefix + byte). Documented in the CLI help.\nconst DETACH_PREFIX: u8 = 0x02; // ctrl-b\nconst DETACH_KEY: u8 = b'd';\n\n/// How long the main loop blocks for an inbound event before looping back to\n/// drain stdin — the full-duplex interleave granularity.\nconst POLL_SLICE: Duration = Duration::from_millis(40);\n\n/// A message from the stdin reader thread to the pump.\nenum StdinMsg {\n    /// Raw bytes to forward to the session as `AttachRecord::Input`.\n    Bytes(Vec<u8>),\n    /// The operator hit the detach keybind — end the viewport, leave the session.\n    Detach,\n}\n\n/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\n/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\n/// events to us instead of scrolling its own buffer — so to keep scroll working we\n/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\n/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\n/// (scanning output); the Windows stdin reader READS them to decide whether to\n/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\n/// all platforms (the scan is cheap + keeps the model correct); only the Windows\n/// reader acts on it (Unix terminals scroll natively, no capture).\n#[derive(Default)]\nstruct MouseMode {\n    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).\n    enabled: std::sync::atomic::AtomicBool,\n    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a\n    /// scroll only when this is on (the modern encoding the harness will parse).\n    sgr: std::sync::atomic::AtomicBool,\n}\n\n/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\n/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\n/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\n/// across two output chunks (the harness's mouse-enable can land across a decoder\n/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\n/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.\n#[derive(Default)]\nstruct MouseModeScanner {\n    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.\n    carry: Vec<u8>,\n}\n\n/// The longest incomplete private-mode prefix we keep across a chunk boundary. A\n/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about\n/// (runaway) — dropped rather than buffered unboundedly.\nconst MOUSE_DECSET_CARRY_MAX: usize = 32;\n\nimpl MouseModeScanner {\n    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to\n    /// `mode`, carrying a trailing partial sequence to the next call.\n    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\n        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\n        if self.carry.is_empty() && !chunk.contains(&0x1b) {\n            return;\n        }\n        let mut buf = std::mem::take(&mut self.carry);\n        buf.extend_from_slice(chunk);\n        let mut i = 0;\n        while i < buf.len() {\n            if buf[i] != 0x1b {\n                i += 1;\n                continue;\n            }\n            match parse_decset_private(&buf[i..]) {\n                DecsetParse::Complete { consumed, params, set } => {\n                    for p in params {\n                        apply_mouse_mode(mode, p, set);\n                    }\n                    i += consumed;\n                }\n                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the\n                // next chunk; a too-long dangling run is treated as not-a-DECSET.\n                DecsetParse::Incomplete => {\n                    let tail = &buf[i..];\n                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {\n                        self.carry = tail.to_vec();\n                    }\n                    return;\n                }\n                // Not a private-mode sequence — skip this ESC, keep scanning.\n                DecsetParse::NotDecset => i += 1,\n            }\n        }\n    }\n}\n\n/// Result of trying to parse a private-mode set/reset at the start of `s` (which\n/// begins with `ESC`).\nenum DecsetParse {\n    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric\n    /// params, and `set` (`true` for `h`, `false` for `l`).\n    Complete { consumed: usize, params: Vec<u16>, set: bool },\n    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.\n    Incomplete,\n    /// `s` does not start a private-mode sequence (`ESC` of something else).\n    NotDecset,\n}\n\n/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the\n/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\nfn parse_decset_private(s: &[u8]) -> DecsetParse {\n    // Need at least `ESC [ ?`.\n    if s.len() < 3 {\n        // Could be the very start of one split across the boundary.\n        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')\n            && s.get(2).is_none_or(|&b| b == b'?')\n        {\n            return DecsetParse::Incomplete;\n        }\n        return DecsetParse::NotDecset;\n    }\n    if s[1] != b'[' || s[2] != b'?' {\n        return DecsetParse::NotDecset;\n    }\n    let mut params: Vec<u16> = Vec::new();\n    let mut cur: Option<u32> = None;\n    let mut j = 3;\n    while j < s.len() {\n        match s[j] {\n            b'0'..=b'9' => {\n                let d = (s[j] - b'0') as u32;\n                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));\n            }\n            b';' => {\n                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);\n                cur = None;\n            }\n            b'h' | b'l' => {\n                if let Some(v) = cur {\n                    params.push(v.min(u16::MAX as u32) as u16);\n                }\n                return DecsetParse::Complete {\n                    consumed: j + 1,\n                    params,\n                    set: s[j] == b'h',\n                };\n            }\n            // Any other byte ⇒ this isn't a clean private-mode set/reset.\n            _ => return DecsetParse::NotDecset,\n        }\n        j += 1;\n    }\n…\n}\n\n[Showing lines 1-183 of 3214. Use :184 to continue]"}],"details":{"truncation":{"content":"//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\r\n//! local terminal to a broker-held PTY session.\r\n//!\r\n//! **One pump, swappable transport** (the doyle invariant). The attach protocol\r\n//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\r\n//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\r\n//! degenerate single-node case: the operator dials its OWN node (loopback peer)\r\n//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\r\n//! against the same broker — the exact cross-node accept+serve path, no local\r\n//! special-case branch. The one-path invariant lives at this pump, not the\r\n//! transport under the stream.\r\n//!\r\n//! **Detach never kills the session.** The broker owns the PTY; closing our send\r\n//! side ends the viewport with the session still running (`serve_attach` →\r\n//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\r\n//! attach-lifecycle invariant).\r\n//!\r\n//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\r\n//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\r\n//! (and watches for the detach keybind); the main loop interleaves draining that\r\n//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\r\n// [impl->REQ-RC-1]\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\nuse std::sync::mpsc;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\r\nuse spt_daemon::effect::{Minter, MintedOp};\r\nuse spt_daemon::brain::{now_ms, Brain, BrokerEvent};\r\nuse spt_daemon::msg::decode_bytes;\r\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\r\n\r\n/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\r\n/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\r\n/// literal ctrl-b to the session; any other byte after the prefix is sent\r\n/// through verbatim (prefix + byte). Documented in the CLI help.\r\nconst DETACH_PREFIX: u8 = 0x02; // ctrl-b\r\nconst DETACH_KEY: u8 = b'd';\r\n\r\n/// How long the main loop blocks for an inbound event before looping back to\r\n/// drain stdin — the full-duplex interleave granularity.\r\nconst POLL_SLICE: Duration = Duration::from_millis(40);\r\n\r\n/// A message from the stdin reader thread to the pump.\r\nenum StdinMsg {\r\n    /// Raw bytes to forward to the session as `AttachRecord::Input`.\r\n    Bytes(Vec<u8>),\r\n    /// The operator hit the detach keybind — end the viewport, leave the session.\r\n    Detach,\r\n}\r\n\r\n/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\r\n/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\r\n/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\r\n/// events to us instead of scrolling its own buffer — so to keep scroll working we\r\n/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\r\n/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\r\n/// (scanning output); the Windows stdin reader READS them to decide whether to\r\n/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\r\n/// all platforms (the scan is cheap + keeps the model correct); only the Windows\r\n/// reader acts on it (Unix terminals scroll natively, no capture).\r\n#[derive(Default)]\r\nstruct MouseMode {\r\n    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).\r\n    enabled: std::sync::atomic::AtomicBool,\r\n    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a\r\n    /// scroll only when this is on (the modern encoding the harness will parse).\r\n    sgr: std::sync::atomic::AtomicBool,\r\n}\r\n\r\n/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\r\n/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\r\n/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\r\n/// across two output chunks (the harness's mouse-enable can land across a decoder\r\n/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\r\n/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.\r\n#[derive(Default)]\r\nstruct MouseModeScanner {\r\n    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.\r\n    carry: Vec<u8>,\r\n}\r\n\r\n/// The longest incomplete private-mode prefix we keep across a chunk boundary. A\r\n/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about\r\n/// (runaway) — dropped rather than buffered unboundedly.\r\nconst MOUSE_DECSET_CARRY_MAX: usize = 32;\r\n\r\nimpl MouseModeScanner {\r\n    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to\r\n    /// `mode`, carrying a trailing partial sequence to the next call.\r\n    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\r\n        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\r\n        if self.carry.is_empty() && !chunk.contains(&0x1b) {\r\n            return;\r\n        }\r\n        let mut buf = std::mem::take(&mut self.carry);\r\n        buf.extend_from_slice(chunk);\r\n        let mut i = 0;\r\n        while i < buf.len() {\r\n            if buf[i] != 0x1b {\r\n                i += 1;\r\n                continue;\r\n            }\r\n            match parse_decset_private(&buf[i..]) {\r\n                DecsetParse::Complete { consumed, params, set } => {\r\n                    for p in params {\r\n                        apply_mouse_mode(mode, p, set);\r\n                    }\r\n                    i += consumed;\r\n                }\r\n                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the\r\n                // next chunk; a too-long dangling run is treated as not-a-DECSET.\r\n                DecsetParse::Incomplete => {\r\n                    let tail = &buf[i..];\r\n                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {\r\n                        self.carry = tail.to_vec();\r\n                    }\r\n                    return;\r\n                }\r\n                // Not a private-mode sequence — skip this ESC, keep scanning.\r\n                DecsetParse::NotDecset => i += 1,\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Result of trying to parse a private-mode set/reset at the start of `s` (which\r\n/// begins with `ESC`).\r\nenum DecsetParse {\r\n    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric\r\n    /// params, and `set` (`true` for `h`, `false` for `l`).\r\n    Complete { consumed: usize, params: Vec<u16>, set: bool },\r\n    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.\r\n    Incomplete,\r\n    /// `s` does not start a private-mode sequence (`ESC` of something else).\r\n    NotDecset,\r\n}\r\n\r\n/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the\r\n/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\r\nfn parse_decset_private(s: &[u8]) -> DecsetParse {\r\n    // Need at least `ESC [ ?`.\r\n    if s.len() < 3 {\r\n        // Could be the very start of one split across the boundary.\r\n        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')\r\n            && s.get(2).is_none_or(|&b| b == b'?')\r\n        {\r\n            return DecsetParse::Incomplete;\r\n        }\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    if s[1] != b'[' || s[2] != b'?' {\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    let mut params: Vec<u16> = Vec::new();\r\n    let mut cur: Option<u32> = None;\r\n    let mut j = 3;\r\n    while j < s.len() {\r\n        match s[j] {\r\n            b'0'..=b'9' => {\r\n                let d = (s[j] - b'0') as u32;\r\n                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));\r\n            }\r\n            b';' => {\r\n                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);\r\n                cur = None;\r\n            }\r\n            b'h' | b'l' => {\r\n                if let Some(v) = cur {\r\n                    params.push(v.min(u16::MAX as u32) as u16);\r\n                }\r\n                return DecsetParse::Complete {\r\n                    consumed: j + 1,\r\n                    params,\r\n                    set: s[j] == b'h',\r\n                };\r\n            }\r\n            // Any other byte ⇒ this isn't a clean private-mode set/reset.\r\n            _ => return DecsetParse::NotDecset,\r\n        }\r\n        j += 1;\r","truncated":true,"truncatedBy":"lines","totalLines":3214,"totalBytes":8510,"outputLines":183,"outputBytes":8510,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\r\n//! local terminal to a broker-held PTY session.\r\n//!\r\n//! **One pump, swappable transport** (the doyle invariant). The attach protocol\r\n//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\r\n//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\r\n//! degenerate single-node case: the operator dials its OWN node (loopback peer)\r\n//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\r\n//! against the same broker — the exact cross-node accept+serve path, no local\r\n//! special-case branch. The one-path invariant lives at this pump, not the\r\n//! transport under the stream.\r\n//!\r\n//! **Detach never kills the session.** The broker owns the PTY; closing our send\r\n//! side ends the viewport with the session still running (`serve_attach` →\r\n//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\r\n//! attach-lifecycle invariant).\r\n//!\r\n//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\r\n//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\r\n//! (and watches for the detach keybind); the main loop interleaves draining that\r\n//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\r\n// [impl->REQ-RC-1]\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\nuse std::sync::mpsc;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\r\nuse spt_daemon::effect::{Minter, MintedOp};\r\nuse spt_daemon::brain::{now_ms, Brain, BrokerEvent};\r\nuse spt_daemon::msg::decode_bytes;\r\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\r\n\r\n/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\r\n/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\r\n/// literal ctrl-b to the session; any other byte after the prefix is sent\r\n/// through verbatim (prefix + byte). Documented in the CLI help.\r\nconst DETACH_PREFIX: u8 = 0x02; // ctrl-b\r\nconst DETACH_KEY: u8 = b'd';\r\n\r\n/// How long the main loop blocks for an inbound event before looping back to\r\n/// drain stdin — the full-duplex interleave granularity.\r\nconst POLL_SLICE: Duration = Duration::from_millis(40);\r\n\r\n/// A message from the stdin reader thread to the pump.\r\nenum StdinMsg {\r\n    /// Raw bytes to forward to the session as `AttachRecord::Input`.\r\n    Bytes(Vec<u8>),\r\n    /// The operator hit the detach keybind — end the viewport, leave the session.\r\n    Detach,\r\n}\r\n\r\n/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\r\n/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\r\n/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\r\n/// events to us instead of scrolling its own buffer — so to keep scroll working we\r\n/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\r\n/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\r\n/// (scanning output); the Windows stdin reader READS them to decide whether to\r\n/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\r\n/// all platforms (the scan is cheap + keeps the model correct); only the Windows\r\n/// reader acts on it (Unix terminals scroll natively, no capture).\r\n#[derive(Default)]\r\nstruct MouseMode {\r\n    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).\r\n    enabled: std::sync::atomic::AtomicBool,\r\n    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a\r\n    /// scroll only when this is on (the modern encoding the harness will parse).\r\n    sgr: std::sync::atomic::AtomicBool,\r\n}\r\n\r\n/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\r\n/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\r\n/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\r\n/// across two output chunks (the harness's mouse-enable can land across a decoder\r\n/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\r\n/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.\r\n#[derive(Default)]\r\nstruct MouseModeScanner {\r\n    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.\r\n    carry: Vec<u8>,\r\n}\r\n\r\n/// The longest incomplete private-mode prefix we keep across a chunk boundary. A\r\n/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about\r\n/// (runaway) — dropped rather than buffered unboundedly.\r\nconst MOUSE_DECSET_CARRY_MAX: usize = 32;\r\n\r\nimpl MouseModeScanner {\r\n    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to\r\n    /// `mode`, carrying a trailing partial sequence to the next call.\r\n    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\r\n        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\r\n        if self.carry.is_empty() && !chunk.contains(&0x1b) {\r\n            return;\r\n        }\r\n        let mut buf = std::mem::take(&mut self.carry);\r\n        buf.extend_from_slice(chunk);\r\n        let mut i = 0;\r\n        while i < buf.len() {\r\n            if buf[i] != 0x1b {\r\n                i += 1;\r\n                continue;\r\n            }\r\n            match parse_decset_private(&buf[i..]) {\r\n                DecsetParse::Complete { consumed, params, set } => {\r\n                    for p in params {\r\n                        apply_mouse_mode(mode, p, set);\r\n                    }\r\n                    i += consumed;\r\n                }\r\n                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the\r\n                // next chunk; a too-long dangling run is treated as not-a-DECSET.\r\n                DecsetParse::Incomplete => {\r\n                    let tail = &buf[i..];\r\n                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {\r\n                        self.carry = tail.to_vec();\r\n                    }\r\n                    return;\r\n                }\r\n                // Not a private-mode sequence — skip this ESC, keep scanning.\r\n                DecsetParse::NotDecset => i += 1,\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Result of trying to parse a private-mode set/reset at the start of `s` (which\r\n/// begins with `ESC`).\r\nenum DecsetParse {\r\n    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric\r\n    /// params, and `set` (`true` for `h`, `false` for `l`).\r\n    Complete { consumed: usize, params: Vec<u16>, set: bool },\r\n    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.\r\n    Incomplete,\r\n    /// `s` does not start a private-mode sequence (`ESC` of something else).\r\n    NotDecset,\r\n}\r\n\r\n/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the\r\n/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\r\nfn parse_decset_private(s: &[u8]) -> DecsetParse {\r\n    // Need at least `ESC [ ?`.\r\n    if s.len() < 3 {\r\n        // Could be the very start of one split across the boundary.\r\n        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')\r\n            && s.get(2).is_none_or(|&b| b == b'?')\r\n        {\r\n            return DecsetParse::Incomplete;\r\n        }\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    if s[1] != b'[' || s[2] != b'?' {\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    let mut params: Vec<u16> = Vec::new();\r\n    let mut cur: Option<u32> = None;\r\n    let mut j = 3;\r\n    while j < s.len() {\r\n        match s[j] {\r\n            b'0'..=b'9' => {\r\n                let d = (s[j] - b'0') as u32;\r\n                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));\r\n            }\r\n            b';' => {\r\n                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);\r\n                cur = None;\r\n            }\r\n            b'h' | b'l' => {\r\n                if let Some(v) = cur {\r\n                    params.push(v.min(u16::MAX as u32) as u16);\r\n                }\r\n                return DecsetParse::Complete {\r\n                    consumed: j + 1,\r\n                    params,\r\n                    set: s[j] == b'h',\r\n                };\r\n            }\r\n            // Any other byte ⇒ this isn't a clean private-mode set/reset.\r\n            _ => return DecsetParse::NotDecset,\r\n        }\r\n        j += 1;\r\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,null,187]},"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":8510,"outputLines":183,"outputBytes":8510,"shownRange":{"start":1,"end":183},"nextOffset":184}}},"isError":false,"timestamp":1784195843561}}
{"type":"message","id":"647b9785","parentId":"aa15c3cf","timestamp":"2026-07-16T09:57:57.776Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Evaluating echo input extension options**\n\n**Planning isolated daemon refresh test setup**\n\n**Designing real broker refresh test**"},{"type":"thinking","thinking":"**Tracing controller connections in broker sessions**\n\n**Clarifying controller versus viewer connections**"},{"type":"toolCall","id":"call_ppcFRNxB6ugMgGdJDEiJ5AFM|fc_0b42584a8ab0545d016a58ab25a7108199883b77ac5eeea066","name":"grep","arguments":{"i":"Locating controller ownership symbols","pattern":"fn dispatch_spawn|become_controller\\(|fn resolve_subscribe|AttachIntent::Viewer|AttachIntent::Control|same_identity","path":"crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_rl3P2wjh0cpLTFOAy0JPQdUt|fc_0b42584a8ab0545d016a58ab25a7748199b26a769887d8ff42","name":"grep","arguments":{"i":"Locating rc serve attach chain","pattern":"request_attach_endpoint|serve_attach|attach_as\\(|SubscribeReq|by:","path":"crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs;crates/spt-daemon/src/dispatch.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_dV7dVI95t9hfrPJjTNS6kFi4|fc_0b42584a8ab0545d016a58ab25a7808199b8beec575b6f339d","name":"read","arguments":{"i":"Reading session brain API","path":"crates/spt-daemon/src/brain.rs","selector":"930-1060,1420-1480"}},{"type":"toolCall","id":"call_o9Z2g5y18oAabDxYnLGM2eZZ|fc_0b42584a8ab0545d016a58ab25a78c81999bd9f5d08a3483d4","name":"read","arguments":{"i":"Reading refresh CLI responses","path":"crates/spt/src/cli.rs","selector":"4050-4095"}},{"type":"toolCall","id":"call_FOk4POQdrinVMxD6OEsrW27C|fc_0b42584a8ab0545d016a58ab25a79481999c836961c4634157","name":"grep","arguments":{"i":"Finding test package integration deps","pattern":"spt-daemon|mock-adapter","path":"crates/spt/Cargo.toml;Cargo.toml","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":21915,"output":1218,"cacheRead":216576,"cacheWrite":0,"totalTokens":239709,"reasoningTokens":893,"cost":{"input":0.109575,"output":0.03654,"cacheRead":0.108288,"cacheWrite":0,"total":0.254403}},"stopReason":"toolUse","timestamp":1784195843607,"responseId":"resp_0b42584a8ab0545d016a58ab03f37481998fa2751e2056e621","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0b42584a8ab0545d016a58ab0599e4819993e3ce56a2432511","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsWkxp1ZVdJON7V-_GW-okSdjUPtF0mRNpR2QjCtjnQNEfgGNWnt_uviQf3YCL_Txbe2f7GQYLhb8RIXCdGoRCKTeOI9J2tCnVbVz_3sbJvEgKlKz1RZwu4viun8BGKFyFUn06p2QTcUPihDR3TbmveD5ITGAPE3pYt1pAcNe79RKPWJewKFXAQsBia3RTK2r0RWqcErBvocZEv2bLPZBnM8uolKxCQO6CYz8-oWT7bg_5Qg-Qyl4Swu-a37OyIwHjtQdbxLp6Uab_PtuawqeZyWwQlMFA4hQgeckyGP8tj1rGG6irsF9fhNlbQ5YaVYwfvWzDXwVrDhB-oObaXL7cGdZGAUE5F9_Bv5VvH4u2inJlG_NBD92icIlQ8lzk-J0nOVWhNZKIfrDQbRfnyhCXv9-6ih3pjFHiv_IsJRTW8l1areJ74UNfzfqjrsOZmFBh5VlOaSaEi6Q31ljgbrBA4UldXmomDtm4M58SK2ZV40XUnX1CS03aoHUI-3CYrf3-fYjeRhAMi0e42IbgVq8VhdPozmzzZ3mvgSw0DPZ4Pi8o7quhvM3OZ030JS3ddnvRrxd7TZtuUToKzEZGCUkyZb_T1B8eMiKU47SRTsUWLMpPtgXlj0ZxZnBiPveT7V305Dppw1vhPDnBn6hEPK9_OMCV1fRcZvk9HZZVTnwnv9slBdVSdn0xaNHcVmOWFcJI4a7hvXg70KVpZvX8GI05TTMP4CeGL4vISZG19FMS509B33iPoRXO-KzZi5TlrDPqgu61GczRrfwzRP51iNqnhEzwwxog5UKxWwQM6bMsxsKBbCGAw_b6gJ0wb6324CI1Sad8GK_5xPfNNw99MxOL789gnJKhcAhzQ6cXDNdDk9Cene52eIS51DVHOG-2ViFtS9TESGa04khN-7ZizYMYXdrXaC0BdSriOQ4u2roqf44ZSfJ367GhIFQqF9R-JAb0g7am2rn5ro-QpVTWha1ZGjlZMfpWgcEM1xmvw6zoTb5VPUR-atLTwUk-XfYUCZaJyslrKAuh_o4nH3IJgI64Da-IdSVEPHQySnsYCnOhogPw6Kaukxbp9zMAd16eIvRWD0Apt7OFH_NB8a_OQd4f1r0ikLQCwH0wpFRT-twY5bdeis-dEmuC_FzvTqbZQVBRRY-QqjHHAtVWsOQp1WOJprWceQoIaGsoNfcmu1LExJ0lFjvOLe4ZpxVyWjvPUzf6WkEvYEC9iGpnlSim74JKHjmkByvRhFCkn4v-3t3NeMxN5N1i1AxgcT0JJzIi1cfPE0gZLCTnlvIzsN879nVYtH3w002eZCc6i62ztk5ibPWAWXusCO5QDj5XAu1Vg6KLTBkYy0CIXhcyB-PUfx0TgQ_2YAdPvo8DIDs_MLTD8GdUs1YbMRRq_g3nKGWw3izwykR7TQOspAkdvWm-PUbDUR0cY70atPgErMkIqAPne2Ea73Yf5jcYFsDm5tCMym6evkfQTZhXqbklihja5-PjzDijT-MyYLxSSfIVTyFvcWv-FyjxeB5Bk5J0sUdtds-YkJD_tSVop_LAHTotMwwNi6JsJYOS8vfB-ybGZBee2kPhy3pE-fe3mTQZbiDtK_Zek-rWBduP25_9SQgOK7JhGqS3todqOQORS13Vs33rpntYns_96QsZ44hpE1nEhoWdRzAU_RfA7a-XUHYYF1Nj6QVU7QIEOZ4seZCy1GZALok7w4duyvQNjatFYRlcpIgIRkCeR1RDWOvX8a5yUAmw0xMmWnE9EWrGOBlPOq8hS3WqJLY8Tn5G2lE3U9Nz8EgtO5I4bH6lXQFXa3go-AwVFYnqG44bCBThBPbS4yZ8AEyBLREtTRz4syP-uVU_dCftXMUVdisAbviSUNjEY-IhuQHX943lJz9BMQ8vC_2n--U4RTFb2l4C4Js6kSnBpB0O5oPtQQSuucpM93zzSSqYhF6kyOK1aY_ujybLqDseIs30rR9ITKHLHOn9ZqRhR5zq-mnR5YpAEX01VqvYrv6oyAvplL1sQho_8ZAiir8ONTtL9So-k2UTqSGY6POMbfjJDXsNxOgR_Ki4Iq_hPJNevPhOR05wmM7e7l-iBp9fwpxJAI3c4VYzX48lP3TCJr6-i9SCNZSQCQYl_vn_hEmP5xiW46ZFq_SMCuxI_uJ4ib6w3iGCnBfrvMQDvAaXUo4j-fm9R7kkBA-7AFHB8ZxQCEU_BVjiv6D_BLxDzxXYXFtMhJlWqsA_hngNucZZ3m_FxALezda-zWc6c0ax3KqbsWkd4pvGtVvHC8U3LmqQYucGDwGi2H7kMg-fT60Ck1pu86zaUgkCM1IOkpiDrpGpp9MxLlbtHPVLn_nrH5wCu0_EQLfldS5KRJk_6FG_M8JTvKdIRur1Hlns3NDRBvooh3TURoG_hmd3KWXIo4LDUv98dquqykZLB_0gBhKxggE_p6m-M4ySdlZgUy9knvWv4Mg9FOvhicwR05Ck8ybtplINrtro59fd44Bthibv6toJ5QubkJtpK9jw_FoEUeNCFzI2EnsM3Tm4PY5u2piNM-Ivtn-Om39Otv6Lza0XViDiFXOZhsUaU8jadTPWO7vBtIxLcSL65ipr5DJnYjuSptJzTf5Ap58RsRIewRBI5_ydG4m4rXa5xZbTVxNb-EIGiqrBtjn3tWAw_tevhi1Z3MQDi7LqYzy5NnnExEh3xB6ahZ4Q6gPjb-9-Xe-msxtl7UjAqrDC0Gu5_IzYSnuLqtBzkKZkvYAG8S_8QOMbn4AatxkFsIDJ3Zzw3LYfB-fBvyAxFKInUvhZT36ufMb5v7gV1cq0PBx6LEIjJwZHHRVIxegsyYO4R7DdfkcIyw2_Hefly_8AI9xCn_K4wNBHSnBopsz5UQSjTsXmN14Dx9Cg5GnOZ9MeJoMl0IgB9VbEL15Y7m9UqFivV3aLG0TKqKatDDmrbMoQKw21gR2TwblNvN-Yv6Va4yanVZo8i45ZkKG9YnNfal7nwAV_OD6cQnHzTEs0pKOJY9J3AYNZQ04XIhlaWJMpCaWKPCsYj5_NQr_eJBNm7DqgTRMSeawcqFUsKMNFeJdBy8j0HJB9sW6OaJRI9pMRjH8ubKTN8TBfop4CQi3jRgBHxsLzHWJ4PAi4DFzYjwYzmA5KWnk89WYzq2fuLUdN3b-79uiMAVrXO7oPRJSgfiYNLHJGKLA9K16GOrz-TJmWUTtSeSg4TjyNkT-5fn6s4vllXzC1d2SEiQwxJIZbhlNDH0eYLG9pAlnwEkfT8v5NVnCLpCT7PZZ7Aw2EKyiyWfsRrP3rH2M9COgeiGfZthxq0cYcRXNyKkcpFpSp6xVSiCsf0wI5kPQwfiovWaL4SqeldacEvhEDkTq1rJb_8XbptQ_3rPm04KfMSMV8v91CMubZ75HWYaJR9qle0VNE9-LBt5jEH_0FccxjtIsqA8PxZlAWpBJERxDfUp3qEbW1yrlo8SMsGlhQ4RZCPiaG0jz8Y7XfqK6jn5FruEMqROjmotyJt1W38zxoO1qHHJTM283tDHrNbQkMaZzseWmBL7uJ00vhP8xC_2P7BT45b3tbm_JFLnFkL3jhlKlPeXiVCajKJzfVNN_sfboUrOXdjQOKGoKySWdle6b0enVKgk_AN8TjbgD5OdHLG9pOd4-uyftOKguuIGnJvHAmkrpgKN7gfL-uBeWsq_ATGKYys2DYabFxjy7weFIW3awAytcqmOqJVxVyX1JVZMqxh4tncj0WGTWcATsKpJDkeHQmdlxVUwKOKEUmcmdV-QgfNLghitPJjYqQ1mi-5gZqiZ54B7eh-FLuUqBe24vDOUequgWI-6wsh9h9nOxykQ3E8thfdrfo9nooaZAPAty8SN6kaMZVp3Y2LXYmIW8umID-zMP9i6ztS45G-q31szetsUyADBvWAu3ceglXLkkn610rH6Fm9T3DwBORbECTZFUhuluQuTDVoiTtREBv9sS-37HZL2lBuJiv9QyYwSE4I-ccB0im73Vne_gv4NXOFdIX8cukHcbABI6tAHTtINlBn7FawvZHvPuNgmYZVuRLNl41vZ12zTX4l5K6P54aaicJiVaX5q7dsOmjKQASZ56h4PXAHU31S0lI3iX7hy6TTqcZqazr70fu-0CZUmcp9knFrk3XBIcNGIrUVitJIz2LmAujSDAeMC3G5YXfmuT6m7420D1niZM0w_qqLUiPKz5vNLS8RwqjviS474zP6ET4rxdv_1xWmh427CSG6q1dOeQAJlevyWOpo3D6I0YDHJv_pbkbJd9BnZdpFcSKo9lCTh2I8EW90v4ZbxFwmqs_5dxAp-PK6JfrHXvai0U2sYyZMLwt2Ci1vXdctZ6U=","summary":[{"type":"summary_text","text":"**Evaluating echo input extension options**"},{"type":"summary_text","text":"**Planning isolated daemon refresh test setup**"},{"type":"summary_text","text":"**Designing real broker refresh test**"}]},{"id":"rs_0b42584a8ab0545d016a58ab16bb488199befe6c930b3440b1","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKslYTqi9LqBpW--0WhIIFGto0gJQR7ApqGQCC35xqIUNANQrcnKbrNdr50aNg4C50753I40b8_d_o439FlyvMYCKwWFFhvWGZa5wwHtFYo83WNh8gB7UlR9N6azHXIY9Rm0Sw39jtBl6qI1Z16j2l1P3ZIFEVsxsbPWsfOtScHIVlAYn1jA4xfo07kuG8NBtOxvIQWWM4tLmjXcPN_xCXVWp96sjx6wKQ3z0mga6ojvzafoaawmSFdI3faMfIfZAqIYwj1dNFlBaKFetDvfbaEI29bae0txKSFDZoCpGQVu8AkA_gvaep99ucLqbk62-LJcMBzOBQ1w9zB9cZwz8s-p_04czh9atPMWNtZKQ9m-WCHt8H824FzkaoS1IPL3lNazfJYT-ZK58FRWATtYmGXki2czmSPGJymu8g0VzAsVJRdPWwpSyl2_n625RtOBwv2o423-DXhylN4_hC6USPmpHKG3iyTm3dJQNokmqVb6yn5Z1XKFCeNvkrcRZpUmA1UGGxcP1_gPUztEd9uxSDieyl4-SS9ii_nbnWqj4DIdNvMDStuUBWIpYbhWMh3yKuW4iPh12n_V8S31cfZEfLiFXl5N7z76VnIcP8LgdsPbwcFmhmvW5PzyCDIabGGGLDE6qYp5Ch0nmSyOFErHx0zym7grZ2zZAnJLhSw66G-70I5x1WhviiuoXDkqAHx8l2IKckBPSKG4C1pfNpWo7pSUeuykefyovjyWKot4FoAcgv4P0DdE8bGlCHqzZf_ROEVZnazgB_FRLGE1zRlD6LghZmKGLXwUJ5Sxif3HRDkGvJKyrP_BhgwVawm2qRzznDgoOvEPP4_puxlHIf4YPj8EuMIiOL4Aa9ZK2MKXAHtAUof9Pg_-4hiJgdeMTQvBHJzd6UmmQD7n0v0tU-1t_fcfVjdzJRg3JeUuqmVxtckBFe5AyqzX-vDzZ2S0M6dz63zezpxeyNciiSuEe1UvGT1fQ8QUIRj4ssIYAQWsAORqUe0kg0yoy6lxS8Ux39g63Y8Few_IXpb8aMubn57G93cEmXXcogIYfOh85PIRQLmzd16-mcMupURgAeNKbVob-QDlaOC_SVv5MyUN8qh0dvox0eboc64xlWkxUSVdnfm81iBSpMba5ymsMbwxDpaZYnSUZHwJ9aIe7s6xWvxBHRsfVuFHpdRamjraeC-u4vt7KMDTheeZ_bJLWDVgr5iKkNKKhXZkP5bK3IW0gl3MPJFl3A1A2K41rQ6b5MHKhzFR32RezElKopBeVTUF0pX94xpw0u7d_jVt_PdYGIEOKpPKF8RSah2mBqsBMG2CIfhv9waHIal9PNDV74mtafTnqFudNMh7xPiCoZjD_pRItH8ZJ_E4nlWFv5Dfm0uIu2mXTLxQE1aPdOiMYr3-nYW-bdWw7TFHS8YTVgLXHNrhG58PzIaT2z9nNt-6dK7eEQjPPLCaOV881Bn_ccvVduZm5XRPEWUVXLScDAK93I082udoI4Q5dno2sUCx-qiZ0Pvtp4EVlqSKvT_lJIhwwArOGjP1yq0SzPaQmjP29S7h1YN7Td04O9rn1wl6Whgx-4wNrWKCd0Vww_aA3Wvkx9Ke4tA4v6AIvHhHrOkMlua2qpUzZC1HqvZtHp4UVliHWsRT_R6fm-l5VHt9SjWEHfuW9ME3MIhctZtoO_gDdvpNhwkUZlVKtkZp8yAd35m9z6vHZJKHboeyqG-VPrfppjJ1oKvXmAXKhAlmOnRsl881lLIbr3bFGzN0EHJTFRgQR6iZeVCi0u8yCgn1EXyWLJO75TZQK77DzzdU13noazpRvURKgwZDuM4EaWUUIxO3QfLO0ipYLUlSZKnodfqeZhtJTrbWQhGzPnRGoVbUQnXgrP9EpbAK8kJSY-8Re9RDsSW0ROLmr_1Zj1JH68RUaXunFmtJpnPhifEDQSSGYJDcPvukF1nX6VrWvr6Ns13CE3fP-Y_rbyFx-TSxtVpMCaNcIEiym4X1Hyf6rue2eYpZDCx26DVABn8IaaUJ_-rMbXiCjoBFTbIvdF0gQuLBBMGDGBWDyc9sJMjFKwZXYMh9Zb3XBGuEpQmlrx4X0Z4RMnuP0pISQW1ymVkX9KDdA8umsZVNGwiQzzbCI6C01F08GuwSrgjM_2fNDQi5YMCY_Nzo9_RFKIH1oh7QKAz8DL3Dg7oZ89WjJZc4TZgkxz_d1LzlibWz9NJjq2LJLvnlQsBIRSaN9m65XfpHzSg0x4jsZJIM5xHAAIxaqGpD39LnTjLPaOV_ylD1P1vtw5EOWsST_oQXTz23mdaVOZLRk_8pBSsFVKDusKgaOTj0DHR1_Qa-9vTtgIoELylzM_IqYIRD5IiBU_xoS9mgzFSI4jyHVpuJindOahcXKch2Rrr_9keo68ORa2-lPlgGM7ZNBhil22WDoQzHNWJKqcnZFnUDeOVsQzR1AVOLtoO7QKP2lhGrHkAxf7rr3nJRI1GcS9785JWH2s01v-T9oPsdKOKYlRhyX6Z5sEz9HaQq0mbCZJjs0KMimkATfL-pDUXAFjwwWEEadDwHO_YPdNFTEgCNabB_yImWht0jNEXuT_4hAjkTxwTptC20ZI2D8ccC2EfoqlnTZZKihVQCf2lfOIX5PStotHW-4jmZBM51I0deNpKf1o5DFLbb62l9TvhefphnusXDFdMVPBoGyerqRvEZbNRnN8CaHMVFbuGKqgVlMB4pq484eFCnfq9fiBwUTODf_hJGJFg-KCDp5OqUsVVlpgsmDO6R7gK83JUmIkuhT4USmWIc-8_CHwE_sK7dBxlxBvx96cDYdfF9CeXqe0-NMaMnybCm30bmi24ji5dNtuDyPQBVWwtS_Rj_5dBWZr9YuN2hwKAIGHQ_mab1rnYE-MuptJpPiSlDKCU8H5Rc7E1xXGj3xvxOwNB8F09umO2upNC3ZjQP6-HqSikBasdVoV0l8IZ9W32t92n7qUma0OzWYMD2Kyh1OunwK7bmNGIDBDMkVn2wD7EnBzI_aRiWbDw0oeLGwlRCM-tbj-eCBTvCXWhwW9YrKGzc_abMEa6UetLwbaTqEeFh6dqsC3uUJV8Fp4hhq769UQR9SeRGuNdgVitJEWFy_3JEiGesBBUHIGpJ5c9HbiHheXPib4AnnMBGxUWloW416DfNBgbNkYdODZeknaq9Z_T2wmCE7begZ22ZcXUGRSyE04mPokF2-iF7MwX0qUUusJiaG9UFi08GHPjttBYLSEnuQVMUGkpkCXGMYmvOEQpWT7AjUwPkKNeuovtw8LXnWl--qJYjG7VTtLwHNCkWWWQtb62_nSh5WOKk_3MMV2_8Sza6gMnNWKtC_7ddsfgJOkUA6BB9r27A_ZFEVFyynnLmrCyAo201bSMMICKe9IQZLo4bN7a53xsmOWVtc2mnNOj7OKLZZ75TSmCQ39FkuZ8dB2UKrjWm0eaPzPiWshSvUenZ1MIC8dr1","summary":[{"type":"summary_text","text":"**Tracing controller connections in broker sessions**"},{"type":"summary_text","text":"**Clarifying controller versus viewer connections**"}]},{"id":"fc_0b42584a8ab0545d016a58ab25a7108199883b77ac5eeea066","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating controller ownership symbols\",\"pattern\":\"fn dispatch_spawn|become_controller\\\\(|fn resolve_subscribe|AttachIntent::Viewer|AttachIntent::Control|same_identity\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_ppcFRNxB6ugMgGdJDEiJ5AFM","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58ab25a7748199b26a769887d8ff42","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating rc serve attach chain\",\"pattern\":\"request_attach_endpoint|serve_attach|attach_as\\\\(|SubscribeReq|by:\",\"path\":\"crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs;crates/spt-daemon/src/dispatch.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_rl3P2wjh0cpLTFOAy0JPQdUt","name":"grep"},{"id":"fc_0b42584a8ab0545d016a58ab25a7808199b8beec575b6f339d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading session brain API\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"930-1060,1420-1480\"}","call_id":"call_dV7dVI95t9hfrPJjTNS6kFi4","name":"read"},{"id":"fc_0b42584a8ab0545d016a58ab25a78c81999bd9f5d08a3483d4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh CLI responses\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"4050-4095\"}","call_id":"call_o9Z2g5y18oAabDxYnLGM2eZZ","name":"read"},{"id":"fc_0b42584a8ab0545d016a58ab25a79481999c836961c4634157","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding test package integration deps\",\"pattern\":\"spt-daemon|mock-adapter\",\"path\":\"crates/spt/Cargo.toml;Cargo.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_FOk4POQdrinVMxD6OEsrW27C","name":"grep"}]},"duration":34154.346200000495,"ttft":1968.9322000006214,"contextSnapshot":{"promptTokens":238491,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ppcFRNxB6ugMgGdJDEiJ5AFM|fc_0b42584a8ab0545d016a58ab25a7108199883b77ac5eeea066","toolName":"grep","startedAt":"2026-07-16T09:57:57.776Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Locating controller ownership symbols"},"id":"f369bb11","parentId":"647b9785","timestamp":"2026-07-16T09:57:57.776Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rl3P2wjh0cpLTFOAy0JPQdUt|fc_0b42584a8ab0545d016a58ab25a7748199b26a769887d8ff42","toolName":"grep","startedAt":"2026-07-16T09:57:57.776Z","args":{"path":"crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs;crates/spt-daemon/src/dispatch.rs"},"intent":"Locating rc serve attach chain"},"id":"a3231e95","parentId":"f369bb11","timestamp":"2026-07-16T09:57:57.776Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dV7dVI95t9hfrPJjTNS6kFi4|fc_0b42584a8ab0545d016a58ab25a7808199b8beec575b6f339d","toolName":"read","startedAt":"2026-07-16T09:57:57.776Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading session brain API"},"id":"064e078a","parentId":"a3231e95","timestamp":"2026-07-16T09:57:57.776Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_o9Z2g5y18oAabDxYnLGM2eZZ|fc_0b42584a8ab0545d016a58ab25a78c81999bd9f5d08a3483d4","toolName":"read","startedAt":"2026-07-16T09:57:57.776Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading refresh CLI responses"},"id":"0d5ae11b","parentId":"064e078a","timestamp":"2026-07-16T09:57:57.776Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FOk4POQdrinVMxD6OEsrW27C|fc_0b42584a8ab0545d016a58ab25a79481999c836961c4634157","toolName":"grep","startedAt":"2026-07-16T09:57:57.776Z","args":{"path":"crates/spt/Cargo.toml;Cargo.toml"},"intent":"Finding test package integration deps"},"id":"328e328a","parentId":"0d5ae11b","timestamp":"2026-07-16T09:57:57.776Z"}
{"type":"message","id":"936ce627","parentId":"328e328a","timestamp":"2026-07-16T09:57:57.784Z","message":{"role":"toolResult","toolCallId":"call_ppcFRNxB6ugMgGdJDEiJ5AFM|fc_0b42584a8ab0545d016a58ab25a7108199883b77ac5eeea066","toolName":"grep","content":[{"type":"text","text":" 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 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 1154|    ) -> SubscribeOutcome {\n*1155|        if intent == AttachIntent::Viewer {\n 1156|            if self.viewers.len() >= MAX_VIEWERS {\n 1157|                // Soft cap reached — refuse the viewer (bounds thread count).\n 1158|                return SubscribeOutcome::BusyControlled {\n...\n 1176|            // Free endpoint → become controller (nothing to displace).\n*1177|            self.become_controller(sub, by, from_seq);\n 1178|            return SubscribeOutcome::Controller;\n 1179|        }\n*1180|        let same_identity = self.controller_by() == by.as_deref();\n*1181|        if same_identity {\n 1182|            // Same operator (or both local) re-serving — silent re-take. This is\n 1183|            // the gate-#7 path: a successor re-taking the slot after a brain\n 1184|            // restart NEVER fires Displaced.\n*1185|            self.become_controller(sub, by, from_seq);\n 1186|            return SubscribeOutcome::Controller;\n 1187|        }\n 1188|        if by.is_none() {\n...\n 1198|            // to notify).\n*1199|            self.become_controller(sub, by, from_seq);\n 1200|            return SubscribeOutcome::Controller;\n 1201|        }\n 1202|        // A DIFFERENT remote controls it.\n...\n 1213|                }\n*1214|                self.become_controller(sub, by, from_seq);\n 1215|                SubscribeOutcome::TookControl\n 1216|            }\n 1217|            _ => SubscribeOutcome::BusyControlled {\n...\n 3224|    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n*3225|    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3226|        let req: SpawnReq =\n 3227|            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n 3228|        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\n...\n 3332|            .unwrap()\n*3333|            .become_controller(Arc::clone(send), None, 0);\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...\n 5500|    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n*5501|    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n 5502|    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n*5503|    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\n 5504|    ///     `controller_epoch`, SUPERSEDING writer-A.\n 5505|    ///\n 5506|    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n...\n 5547|        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\n*5548|        log.become_controller(Arc::clone(&send), None, 0);\n 5549|        // A further chunk advances the grid to a NEW current screen (seq 2). The\n 5550|        // handoff to A's (pinned) channel is buffered; A never gets to write it.\n 5551|        assert!(\n...\n 5556|        // epoch; writer-A is superseded. Both writers park on the pinned socket.\n*5557|        log.become_controller(Arc::clone(&send), None, 0);\n 5558|\n 5559|        // Release the socket: the writers race for the mutex. The in-lock gate makes\n 5560|        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\n...\n 5615|        // Cold controller attach from seq 0.\n*5616|        log.become_controller(Arc::clone(&send), None, 0);\n 5617|\n 5618|        // The initial batch is ONE output frame carrying the synthesized repaint.\n 5619|        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\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":17,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":17}],"truncated":false,"displayContent":"  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 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 1154│    ) -> SubscribeOutcome {\n*1155│        if intent == AttachIntent::Viewer {\n 1156│            if self.viewers.len() >= MAX_VIEWERS {\n 1157│                // Soft cap reached — refuse the viewer (bounds thread count).\n 1158│                return SubscribeOutcome::BusyControlled {\n     │...\n 1176│            // Free endpoint → become controller (nothing to displace).\n*1177│            self.become_controller(sub, by, from_seq);\n 1178│            return SubscribeOutcome::Controller;\n 1179│        }\n*1180│        let same_identity = self.controller_by() == by.as_deref();\n*1181│        if same_identity {\n 1182│            // Same operator (or both local) re-serving — silent re-take. This is\n 1183│            // the gate-#7 path: a successor re-taking the slot after a brain\n 1184│            // restart NEVER fires Displaced.\n*1185│            self.become_controller(sub, by, from_seq);\n 1186│            return SubscribeOutcome::Controller;\n 1187│        }\n 1188│        if by.is_none() {\n     │...\n 1198│            // to notify).\n*1199│            self.become_controller(sub, by, from_seq);\n 1200│            return SubscribeOutcome::Controller;\n 1201│        }\n 1202│        // A DIFFERENT remote controls it.\n     │...\n 1213│                }\n*1214│                self.become_controller(sub, by, from_seq);\n 1215│                SubscribeOutcome::TookControl\n 1216│            }\n 1217│            _ => SubscribeOutcome::BusyControlled {\n     │...\n 3224│    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n*3225│    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3226│        let req: SpawnReq =\n 3227│            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n 3228│        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\n     │...\n 3332│            .unwrap()\n*3333│            .become_controller(Arc::clone(send), None, 0);\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     │...\n 5500│    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n*5501│    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n 5502│    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n*5503│    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\n 5504│    ///     `controller_epoch`, SUPERSEDING writer-A.\n 5505│    ///\n 5506│    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n     │...\n 5547│        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\n*5548│        log.become_controller(Arc::clone(&send), None, 0);\n 5549│        // A further chunk advances the grid to a NEW current screen (seq 2). The\n 5550│        // handoff to A's (pinned) channel is buffered; A never gets to write it.\n 5551│        assert!(\n     │...\n 5556│        // epoch; writer-A is superseded. Both writers park on the pinned socket.\n*5557│        log.become_controller(Arc::clone(&send), None, 0);\n 5558│\n 5559│        // Release the socket: the writers race for the mutex. The in-lock gate makes\n 5560│        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\n     │...\n 5615│        // Cold controller attach from seq 0.\n*5616│        log.become_controller(Arc::clone(&send), None, 0);\n 5617│\n 5618│        // The initial batch is ONE output frame carrying the synthesized repaint.\n 5619│        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\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":1784195877784}}
{"type":"message","id":"25bd6743","parentId":"936ce627","timestamp":"2026-07-16T09:57:57.786Z","message":{"role":"toolResult","toolCallId":"call_FOk4POQdrinVMxD6OEsrW27C|fc_0b42584a8ab0545d016a58ab25a79481999c836961c4634157","toolName":"grep","content":[{"type":"text","text":"# Cargo.toml\n 1|# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:\n*2|#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt\n 3|# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).\n 4|# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.\n 5|# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.\n...\n 7|# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.\n*8|# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).\n 9|# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.\n 10|[workspace]\n 11|resolver = \"2\"\n*12|members = [\"crates/spt-proto\", \"crates/spt-store\", \"crates/spt-msg\", \"crates/spt-net\", \"crates/spt-term\", \"crates/spt-runtime\", \"crates/spt-live\", \"crates/spt-daemon\", \"crates/spt\", \"adapters/mock\", \"crates/xtask\"]\n 13|\n 14|# M6-D6 (grill decision 9): ONE shared version for every crate and the binary\n 15|# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol\n\n# crates/spt/\n## Cargo.toml\n 17|# cross-crate fixture-bin pattern as `mock-session`). The name MUST stay distinct\n*18|# from spt-daemon's `xlate_choreo_fixture` bin: an identical bin name collides on\n 19|# the shared `target/<profile>/` output filename and the two clobber each other —\n 20|# the daemon's `g3_content_free_ping` then resolves `CARGO_BIN_EXE_xlate_choreo_fixture`\n 21|# to the wrong fixture and fails (the PR#28 Windows gate-escape).\n...\n 63|spt-live = { path = \"../spt-live\" }\n*64|spt-daemon = { path = \"../spt-daemon\" }\n 65|# The `api digest-entry` push door validates the inbound record against the\n 66|# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.\n 67|spt-term = { path = \"../spt-term\" }\n...\n 87|# The WAN send leg (D9-1-4) serializes the dial address + reads registry\n*88|# snapshots - same pinned shape as spt-daemon's.\n 89|serde_json = { version = \"1\", features = [\"preserve_order\"] }\n 90|# `--json` read/status DTOs (REQ-CLI-JSON): committed, hand-chosen output\n 91|# shapes derive Serialize here; serde_json above is the encoder/sink.\n...\n 140|# the lib's normal deps, so the daemon/net/identity surfaces it touches directly\n*141|# are re-declared here (the spt-daemon dev-dep pattern).\n*142|spt-daemon = { path = \"../spt-daemon\" }\n 143|spt-net = { path = \"../spt-net\" }\n 144|spt-proto = { path = \"../spt-proto\" }\n 145|# docs_bundle_e2e signs a test update set exactly as propagate.rs does —\n*146|# same version the workspace already carries via spt-daemon.\n 147|ed25519-dalek = \"2\""}],"details":{"scopePath":"crates/spt/Cargo.toml, Cargo.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":9,"fileCount":2,"files":["crates/spt/Cargo.toml","Cargo.toml"],"fileMatches":[{"path":"crates/spt/Cargo.toml","count":6},{"path":"Cargo.toml","count":3}],"truncated":false,"displayContent":"# Cargo.toml\n  1│# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:\n *2│#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt\n  3│# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).\n  4│# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.\n  5│# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.\n   │...\n  7│# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.\n *8│# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).\n  9│# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.\n 10│[workspace]\n 11│resolver = \"2\"\n*12│members = [\"crates/spt-proto\", \"crates/spt-store\", \"crates/spt-msg\", \"crates/spt-net\", \"crates/spt-term\", \"crates/spt-runtime\", \"crates/spt-live\", \"crates/spt-daemon\", \"crates/spt\", \"adapters/mock\", \"crates/xtask\"]\n 13│\n 14│# M6-D6 (grill decision 9): ONE shared version for every crate and the binary\n 15│# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol\n\n# crates/spt/\n## Cargo.toml\n  17│# cross-crate fixture-bin pattern as `mock-session`). The name MUST stay distinct\n *18│# from spt-daemon's `xlate_choreo_fixture` bin: an identical bin name collides on\n  19│# the shared `target/<profile>/` output filename and the two clobber each other —\n  20│# the daemon's `g3_content_free_ping` then resolves `CARGO_BIN_EXE_xlate_choreo_fixture`\n  21│# to the wrong fixture and fails (the PR#28 Windows gate-escape).\n    │...\n  63│spt-live = { path = \"../spt-live\" }\n *64│spt-daemon = { path = \"../spt-daemon\" }\n  65│# The `api digest-entry` push door validates the inbound record against the\n  66│# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.\n  67│spt-term = { path = \"../spt-term\" }\n    │...\n  87│# The WAN send leg (D9-1-4) serializes the dial address + reads registry\n *88│# snapshots - same pinned shape as spt-daemon's.\n  89│serde_json = { version = \"1\", features = [\"preserve_order\"] }\n  90│# `--json` read/status DTOs (REQ-CLI-JSON): committed, hand-chosen output\n  91│# shapes derive Serialize here; serde_json above is the encoder/sink.\n    │...\n 140│# the lib's normal deps, so the daemon/net/identity surfaces it touches directly\n*141│# are re-declared here (the spt-daemon dev-dep pattern).\n*142│spt-daemon = { path = \"../spt-daemon\" }\n 143│spt-net = { path = \"../spt-net\" }\n 144│spt-proto = { path = \"../spt-proto\" }\n 145│# docs_bundle_e2e signs a test update set exactly as propagate.rs does —\n*146│# same version the workspace already carries via spt-daemon.\n 147│ed25519-dalek = \"2\""},"isError":false,"timestamp":1784195877786}}
{"type":"message","id":"dc7f8449","parentId":"25bd6743","timestamp":"2026-07-16T09:57:57.787Z","message":{"role":"toolResult","toolCallId":"call_rl3P2wjh0cpLTFOAy0JPQdUt|fc_0b42584a8ab0545d016a58ab25a7748199b26a769887d8ff42","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### attach.rs\n 160|    session_id: u64,\n*161|    by: Option<&str>,\n 162|) -> io::Result<BrokerEvent> {\n 163|    let mut last_resume_floor: Option<u64> = None;\n 164|    let mut swallow_resume_ack = false;\n...\n 184|                // frames from the ring.\n*185|                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n 186|                swallow_resume_ack = true;\n 187|            }\n 188|            Err(e) => return Err(e),\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 378|                            } else {\n*379|                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n 380|                            }\n 381|                            attached = true;\n 382|                        }\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 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### dispatch.rs\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 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\n## spt/src/\n### rc.rs\n 4|//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n*5|//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\n 6|//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\n 7|//! degenerate single-node case: the operator dials its OWN node (loopback peer)\n*8|//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\n 9|//! against the same broker — the exact cross-node accept+serve path, no local\n 10|//! special-case branch. The one-path invariant lives at this pump, not the\n 11|//! transport under the stream.\n...\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...\n 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 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 1840|    }\n*1841|    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n 1842|    // (the session outlives the viewport). Best-effort: an already-finished or\n 1843|    // exited stream send is harmless to attempt.\n 1844|    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {"}],"details":{"scopePath":"crates/spt-daemon/src/attach.rs, crates/spt/src/rc.rs, crates/spt-daemon/src/dispatch.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":15,"fileCount":3,"files":["crates/spt-daemon/src/attach.rs","crates/spt/src/rc.rs","crates/spt-daemon/src/dispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/attach.rs","count":6},{"path":"crates/spt/src/rc.rs","count":6},{"path":"crates/spt-daemon/src/dispatch.rs","count":3}],"truncated":false,"displayContent":"# crates/\n\n## spt-daemon/src/\n### attach.rs\n 160│    session_id: u64,\n*161│    by: Option<&str>,\n 162│) -> io::Result<BrokerEvent> {\n 163│    let mut last_resume_floor: Option<u64> = None;\n 164│    let mut swallow_resume_ack = false;\n    │...\n 184│                // frames from the ring.\n*185│                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n 186│                swallow_resume_ack = true;\n 187│            }\n 188│            Err(e) => return Err(e),\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 378│                            } else {\n*379│                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n 380│                            }\n 381│                            attached = true;\n 382│                        }\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 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### dispatch.rs\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 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\n## spt/src/\n### rc.rs\n    4│//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n   *5│//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\n    6│//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\n    7│//! degenerate single-node case: the operator dials its OWN node (loopback peer)\n   *8│//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\n    9│//! against the same broker — the exact cross-node accept+serve path, no local\n   10│//! special-case branch. The one-path invariant lives at this pump, not the\n   11│//! transport under the stream.\n     │...\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     │...\n   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 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 1840│    }\n*1841│    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n 1842│    // (the session outlives the viewport). Best-effort: an already-finished or\n 1843│    // exited stream send is harmless to attempt.\n 1844│    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {"},"isError":false,"timestamp":1784195877787}}
{"type":"message","id":"3e0f8f01","parentId":"dc7f8449","timestamp":"2026-07-16T09:57:57.800Z","message":{"role":"toolResult","toolCallId":"call_dV7dVI95t9hfrPJjTNS6kFi4|fc_0b42584a8ab0545d016a58ab25a7808199b8beec575b6f339d","toolName":"read","content":[{"type":"text","text":"    pub fn attach_as(\n…\n    ) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\n        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n        // --attach` case) gets a clean-screen repaint at pseudo-seq\n        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n        self.baseline_next_output = from_seq == 0;\n        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))\n    }\n\n    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —\n    /// skip-to-live (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The viewer overflowed its\n    /// bounded subscription queue (it forwards slower than the drain fans out) and\n    /// the broker evicted it (correct session-protection); rather than dying it\n    /// jumps to live and skips the uncatchable backlog (tail -f reconnect).\n    ///\n    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)\n    /// EMPTY, so the viewer receives only the size frame + live output going\n    /// forward. The next live frame carries the current ring tail's seq — a\n    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup\n    /// (the cold serve brain's default) would FATAL that jump as an \"output gap\",\n    /// and a `next_seq = MAX` would DEDUP-DROP every live frame (seq < MAX); so we\n    /// arm the snap-above dedup path (`session_cursors` non-empty) seeded at 0,\n    /// which accepts the jump and snaps. VIEWER-only → B2-safe (a viewer never\n    /// advances `delivered_through` / is not authoritative). The subscribe is sent\n    /// FIRST (a cold brain's empty-map skips `subscribe_with`'s `from_seq` reset),\n    /// THEN the cursor is armed, so neither path stamps `MAX`.\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n    pub fn attach_skip_to_live(&mut self, session_id: u64, by: Option<&str>) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.subscribe_with(session_id, u64::MAX, AttachIntent::Viewer, by.map(str::to_string))?;\n        self.session_cursors.insert(session_id, 0);\n        self.next_seq = 0;\n        Ok(())\n    }\n\n    /// Arm SNAP-ABOVE at the INITIAL viewer attach — a `rc --view` VIEWER must\n    /// tolerate a **PRE-eviction** forward Output gap. Under a hard flood this\n    /// serving brain can fall behind the live ring and read a `seq` past its\n    /// cursor because the ring rolled the intervening frames OUT between reads,\n    /// with NO channel-overflow eviction and so NO [`KIND_VIEWER_EVICTED`] marker\n    /// preceding the jump. Without arming, the viewer serve-brain leaves\n    /// `session_cursors` EMPTY and runs the LEGACY reject-gap path\n    /// ([`Brain::read_event`] :624/:628), which FATALS the forward gap →\n    /// `serve_attach` returns → forwarding stops → the operator sees nothing\n    /// (a_journaled / p0_paste / attach.rs:1071 wedged_viewer, Linux forkpty;\n    /// MASKED on Windows ConPTY, which floods slower so the ring never rolls far\n    /// enough to gap a still-subscribed viewer).\n    ///\n    /// Mirrors [`Brain::attach_skip_to_live`] but seeds the snap-above cursor at\n    /// the requested `from_seq` (the viewer asked from there — dedup BELOW it,\n    /// snap ABOVE a forward gap) and subscribes from `from_seq`, NOT the MAX\n    /// skip-replay, because this is the FIRST attach rather than a post-eviction\n    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n    /// recovers POST-eviction (re-subscribe from the floor). VIEWER-only →\n    /// B2-SAFE (a viewer never advances `delivered_through` / is not authoritative\n    /// — the CONTROLLER keeps the strict reject-gap for exactly-once resume). The\n    /// subscribe is sent FIRST (a cold brain's empty map skips `subscribe_with`'s\n    /// `from_seq` reset), THEN the cursor is armed.\n    // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n    pub fn attach_as_viewer_snap(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        by: Option<&str>,\n    ) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\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…\n    pub fn net_stream_send(\n…\n    ) -> io::Result<Option<NetSent>> {\n…\n        if op.is_none() {\n…\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    /// Trigger a **planned brain-process restart** on the broker (ADR-0018 D3-3,\n    /// Q3): the broker signals its supervisor to cycle the brain child onto the\n    /// freshly-swapped binary, with `start-reason=update`. `spt update apply`\n    /// calls this after swapping the binary on disk — the seam that makes a\n    /// routine update actually run new code without a manual bounce, every\n    /// broker-held endpoint untouched at the process level.\n    ///\n    /// Returns `honored`: whether the broker had a supervisor wired (false on a\n    /// bare broker with no supervisor, e.g. a test broker — the verb is accepted\n    /// but nothing respawns). An older broker that predates the verb answers with\n    /// an error (KH-2.3 forward-compat: surfaced, never a silent no-op of a\n    /// swapped binary). Readiness-gated promotion / auto-rollback is D6.\n    // [impl->REQ-UPD-3]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.honored);\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"}],"details":{"displayContent":{"text":"    pub fn attach_as(\n…\n    ) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\n        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n        // --attach` case) gets a clean-screen repaint at pseudo-seq\n        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n        self.baseline_next_output = from_seq == 0;\n        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))\n    }\n\n    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —\n    /// skip-to-live (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The viewer overflowed its\n    /// bounded subscription queue (it forwards slower than the drain fans out) and\n    /// the broker evicted it (correct session-protection); rather than dying it\n    /// jumps to live and skips the uncatchable backlog (tail -f reconnect).\n    ///\n    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)\n    /// EMPTY, so the viewer receives only the size frame + live output going\n    /// forward. The next live frame carries the current ring tail's seq — a\n    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup\n    /// (the cold serve brain's default) would FATAL that jump as an \"output gap\",\n    /// and a `next_seq = MAX` would DEDUP-DROP every live frame (seq < MAX); so we\n    /// arm the snap-above dedup path (`session_cursors` non-empty) seeded at 0,\n    /// which accepts the jump and snaps. VIEWER-only → B2-safe (a viewer never\n    /// advances `delivered_through` / is not authoritative). The subscribe is sent\n    /// FIRST (a cold brain's empty-map skips `subscribe_with`'s `from_seq` reset),\n    /// THEN the cursor is armed, so neither path stamps `MAX`.\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n    pub fn attach_skip_to_live(&mut self, session_id: u64, by: Option<&str>) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.subscribe_with(session_id, u64::MAX, AttachIntent::Viewer, by.map(str::to_string))?;\n        self.session_cursors.insert(session_id, 0);\n        self.next_seq = 0;\n        Ok(())\n    }\n\n    /// Arm SNAP-ABOVE at the INITIAL viewer attach — a `rc --view` VIEWER must\n    /// tolerate a **PRE-eviction** forward Output gap. Under a hard flood this\n    /// serving brain can fall behind the live ring and read a `seq` past its\n    /// cursor because the ring rolled the intervening frames OUT between reads,\n    /// with NO channel-overflow eviction and so NO [`KIND_VIEWER_EVICTED`] marker\n    /// preceding the jump. Without arming, the viewer serve-brain leaves\n    /// `session_cursors` EMPTY and runs the LEGACY reject-gap path\n    /// ([`Brain::read_event`] :624/:628), which FATALS the forward gap →\n    /// `serve_attach` returns → forwarding stops → the operator sees nothing\n    /// (a_journaled / p0_paste / attach.rs:1071 wedged_viewer, Linux forkpty;\n    /// MASKED on Windows ConPTY, which floods slower so the ring never rolls far\n    /// enough to gap a still-subscribed viewer).\n    ///\n    /// Mirrors [`Brain::attach_skip_to_live`] but seeds the snap-above cursor at\n    /// the requested `from_seq` (the viewer asked from there — dedup BELOW it,\n    /// snap ABOVE a forward gap) and subscribes from `from_seq`, NOT the MAX\n    /// skip-replay, because this is the FIRST attach rather than a post-eviction\n    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n    /// recovers POST-eviction (re-subscribe from the floor). VIEWER-only →\n    /// B2-SAFE (a viewer never advances `delivered_through` / is not authoritative\n    /// — the CONTROLLER keeps the strict reject-gap for exactly-once resume). The\n    /// subscribe is sent FIRST (a cold brain's empty map skips `subscribe_with`'s\n    /// `from_seq` reset), THEN the cursor is armed.\n    // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n    pub fn attach_as_viewer_snap(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        by: Option<&str>,\n    ) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\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…\n    pub fn net_stream_send(\n…\n    ) -> io::Result<Option<NetSent>> {\n…\n        if op.is_none() {\n…\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    /// Trigger a **planned brain-process restart** on the broker (ADR-0018 D3-3,\n    /// Q3): the broker signals its supervisor to cycle the brain child onto the\n    /// freshly-swapped binary, with `start-reason=update`. `spt update apply`\n    /// calls this after swapping the binary on disk — the seam that makes a\n    /// routine update actually run new code without a manual bounce, every\n    /// broker-held endpoint untouched at the process level.\n    ///\n    /// Returns `honored`: whether the broker had a supervisor wired (false on a\n    /// bare broker with no supervisor, e.g. a test broker — the verb is accepted\n    /// but nothing respawns). An older broker that predates the verb answers with\n    /// an error (KH-2.3 forward-compat: surfaced, never a silent no-op of a\n    /// swapped binary). Readiness-gated promotion / auto-rollback is D6.\n    // [impl->REQ-UPD-3]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.honored);\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","startLine":923,"lineNumbers":[923,null,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,null,1400,null,1406,null,1418,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784195877799}}
{"type":"message","id":"1a7a56fc","parentId":"3e0f8f01","timestamp":"2026-07-16T09:57:57.962Z","message":{"role":"toolResult","toolCallId":"call_o9Z2g5y18oAabDxYnLGM2eZZ|fc_0b42584a8ab0545d016a58ab25a78c81999bd9f5d08a3483d4","toolName":"read","content":[{"type":"text","text":"fn cmd_daemon_stop(force: bool) -> i32 {\n…\n    match spt_daemon::stop_daemon() {\n…\n        Ok(spt_daemon::StopOutcome::IpcStopped) => {\n…\n            0\n        }\n        Err(e) => {\n            eprintln!(\"DAEMON_STOP_FAIL: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon refresh` — cycle the daemon's brain in place with NO binary swap\n/// (REQ-DAEMON-REFRESH): the apply_staged brain-cycle path minus the swap, so the\n/// broker and every hosted PTY survive by construction. Requires a running\n/// daemon (nothing to refresh otherwise — `daemon start` is the cold path). A\n/// broker without a supervisor wired accepts but cannot act; that is loud, not\n/// silent — the operator asked for a cycle that did not happen.\n// [impl->REQ-DAEMON-REFRESH]\nfn cmd_daemon_refresh() -> i32 {\n    if !spt_daemon::is_running() {\n        eprintln!(\"DAEMON_NOT_RUNNING: nothing to refresh — `spt daemon start` brings it up fresh\");\n        return 1;\n    }\n    match spt_daemon::refresh_brain(&spt_daemon::broker_socket_name()) {\n        Ok(true) => {\n            println!(\n                \"Daemon coordinator restarted in place — hosted sessions and connections were \\\n                 not touched. Run `spt daemon status` to confirm.\"\n            );\n            0\n        }\n        Ok(false) => {\n            eprintln!(\n                \"DAEMON_REFRESH_NOT_HONORED: the daemon accepted the request but has no \\\n                 supervisor to cycle its coordinator (an older or unmanaged daemon) — use \\\n                 `spt daemon stop` then `spt daemon start` instead (hosted sessions come back \\\n                 automatically).\"\n            );\n            1\n        }\n        Err(e) => {\n            eprintln!(\"DAEMON_REFRESH_FAIL:{e}\");\n            1\n        }\n    }\n}\n\n/// The connection + peer-pump status lines (REQ-DAEMON-5/-9). Pure for tests.\n/// A net-LESS broker (`net_up == false`) surfaces \"no connection (retrying)\" +\n/// a pump waiting on the network — NOT a STALLED alarm read off a stale\n/// pre-boot heartbeat (the kitsubito 2026-06-08 boot-race misreport, where the\n/// heartbeat age exceeded uptime). With net up, the heartbeat drives\n\n[Showing lines 4049-4098 of 18900. Use :4099 to continue]"}],"details":{"truncation":{"content":"            0\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\"DAEMON_STOP_FAIL: {e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// `spt daemon refresh` — cycle the daemon's brain in place with NO binary swap\r\n/// (REQ-DAEMON-REFRESH): the apply_staged brain-cycle path minus the swap, so the\r\n/// broker and every hosted PTY survive by construction. Requires a running\r\n/// daemon (nothing to refresh otherwise — `daemon start` is the cold path). A\r\n/// broker without a supervisor wired accepts but cannot act; that is loud, not\r\n/// silent — the operator asked for a cycle that did not happen.\r\n// [impl->REQ-DAEMON-REFRESH]\r\nfn cmd_daemon_refresh() -> i32 {\r\n    if !spt_daemon::is_running() {\r\n        eprintln!(\"DAEMON_NOT_RUNNING: nothing to refresh — `spt daemon start` brings it up fresh\");\r\n        return 1;\r\n    }\r\n    match spt_daemon::refresh_brain(&spt_daemon::broker_socket_name()) {\r\n        Ok(true) => {\r\n            println!(\r\n                \"Daemon coordinator restarted in place — hosted sessions and connections were \\\r\n                 not touched. Run `spt daemon status` to confirm.\"\r\n            );\r\n            0\r\n        }\r\n        Ok(false) => {\r\n            eprintln!(\r\n                \"DAEMON_REFRESH_NOT_HONORED: the daemon accepted the request but has no \\\r\n                 supervisor to cycle its coordinator (an older or unmanaged daemon) — use \\\r\n                 `spt daemon stop` then `spt daemon start` instead (hosted sessions come back \\\r\n                 automatically).\"\r\n            );\r\n            1\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\"DAEMON_REFRESH_FAIL:{e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// The connection + peer-pump status lines (REQ-DAEMON-5/-9). Pure for tests.\r\n/// A net-LESS broker (`net_up == false`) surfaces \"no connection (retrying)\" +\r\n/// a pump waiting on the network — NOT a STALLED alarm read off a stale\r\n/// pre-boot heartbeat (the kitsubito 2026-06-08 boot-race misreport, where the\r\n/// heartbeat age exceeded uptime). With net up, the heartbeat drives\r","truncated":true,"truncatedBy":"lines","totalLines":14852,"totalBytes":2076,"outputLines":50,"outputBytes":2076,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn cmd_daemon_stop(force: bool) -> i32 {\n…\n    match spt_daemon::stop_daemon() {\n…\n        Ok(spt_daemon::StopOutcome::IpcStopped) => {\n…\n            0\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\"DAEMON_STOP_FAIL: {e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// `spt daemon refresh` — cycle the daemon's brain in place with NO binary swap\r\n/// (REQ-DAEMON-REFRESH): the apply_staged brain-cycle path minus the swap, so the\r\n/// broker and every hosted PTY survive by construction. Requires a running\r\n/// daemon (nothing to refresh otherwise — `daemon start` is the cold path). A\r\n/// broker without a supervisor wired accepts but cannot act; that is loud, not\r\n/// silent — the operator asked for a cycle that did not happen.\r\n// [impl->REQ-DAEMON-REFRESH]\r\nfn cmd_daemon_refresh() -> i32 {\r\n    if !spt_daemon::is_running() {\r\n        eprintln!(\"DAEMON_NOT_RUNNING: nothing to refresh — `spt daemon start` brings it up fresh\");\r\n        return 1;\r\n    }\r\n    match spt_daemon::refresh_brain(&spt_daemon::broker_socket_name()) {\r\n        Ok(true) => {\r\n            println!(\r\n                \"Daemon coordinator restarted in place — hosted sessions and connections were \\\r\n                 not touched. Run `spt daemon status` to confirm.\"\r\n            );\r\n            0\r\n        }\r\n        Ok(false) => {\r\n            eprintln!(\r\n                \"DAEMON_REFRESH_NOT_HONORED: the daemon accepted the request but has no \\\r\n                 supervisor to cycle its coordinator (an older or unmanaged daemon) — use \\\r\n                 `spt daemon stop` then `spt daemon start` instead (hosted sessions come back \\\r\n                 automatically).\"\r\n            );\r\n            1\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\"DAEMON_REFRESH_FAIL:{e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// The connection + peer-pump status lines (REQ-DAEMON-5/-9). Pure for tests.\r\n/// A net-LESS broker (`net_up == false`) surfaces \"no connection (retrying)\" +\r\n/// a pump waiting on the network — NOT a STALLED alarm read off a stale\r\n/// pre-boot heartbeat (the kitsubito 2026-06-08 boot-race misreport, where the\r\n/// heartbeat age exceeded uptime). With net up, the heartbeat drives\r","startLine":4030,"lineNumbers":[4030,null,4038,null,4047,null,4049,4050,4051,4052,4053,4054,4055,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4067,4068,4069,4070,4071,4072,4073,4074,4075,4076,4077,4078,4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":18900,"totalBytes":2076,"outputLines":50,"outputBytes":2076,"shownRange":{"start":4049,"end":4098},"nextOffset":4099}}},"isError":false,"timestamp":1784195877962}}
{"type":"compaction","id":"ccbb314e","parentId":"1a7a56fc","timestamp":"2026-07-16T09:57:58.106Z","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)\ncrates (Read)\n# .config/\nnextest.toml (Read)\n# .github/workflows/\nci.yml (Read)\n# adapters/mock/src/\nmain.rs (Read)\n# crates/\n## spt-daemon/\n### src/\napplyhost.rs (Read)\nbrainproc.rs (Read)\nbroker.rs (Read)\n### tests/\nattach.rs (Read)\nbrain_decouple.rs (Read)\nbrain_resume_conn_deadlock.rs (Read)\nconn_blackhole_lifecycle.rs (Read)\ndaemon_e2e.rs (Read)\ndaemon_lifecycle_real_brain.rs (Read)\nendpoint_survival.rs (Read)\n## spt/\n### src/\nrc.rs (Read)\n### tests/\nattach_wedge_e2e.rs (Read)\nbrain_split.rs (Read)\nbrain_survive.rs (Read)\ncomposite_e2e.rs (Read)\n[…14 files elided…]\n\nHISTORY\n===================","shortSummary":"Archived 112,098 chars of history onto 7 snapcompact frames (+27,833 chars as text)","firstKeptEntryId":"647b9785","tokensBefore":239709,"details":{"readFiles":[".config/nextest.toml",".github/workflows/ci.yml","CONTEXT.md","adapters/mock/src/main.rs","crates","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.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/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt/src/rc.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/brain_split.rs","crates/spt/tests/brain_survive.rs","crates/spt/tests/composite_e2e.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","docs/DEBUG-ROLLOUT.md","docs/FAULT-MATRIX.md","docs/KNOWN-HAZARDS.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/UPDATE-WEDGE-ROOT-CAUSE.md","docs/W3-WAVE-GATE-REPORT.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0018-broker-brain-process-isolation-restoration.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":"iVBORw0KGgoAAAANSUhEUgAABiAAAAYaAgMAAAAgkEnVAAAACVBMVEX///8AAACAgIBJX6m2AAEcF0lEQVR42uxdS47juBLMIlwAoZUb6N4TXgk+RT1gDlCLrvsQXvkYWhpzyqfMyCST+tiuX3d1jzSYalui+AuRlJWhCKI7tpcX+sLb167dh9b85SXcle6+VH82EOED275Q85hicpnmRPz1OBwv3SAn5OkJ0xy64Zg5VRg+oLVR65P8zkRdLU3+Hi9RmtMNS/Xq/KlLhdBC2rh6ih3Jiyki9lvrk/RIyMehG7h2/17w+Xj59zLWe/x8HBaBCDlkoBoFFgDBvZFie0Jw1YptPTP6MEyaGSksY7fYUmSM+lCODRBhUgvseXnhOmpKLT3I/yV9Xr+G6984ASKu4zZPYcXHCkQaL6fEV1Tk/PWzlJRSaPo11godL8dL3SkArABBt4CY1TuuteQKEFqfe4FwPTG7ZK4A0eb3LiBSmwjDGV0PIPQzgIgLQKTYZYyIy9DRQYZMC0Q38AkHHmTnlDr9JJORfOpkyFUgkILz4TSSarwGkBr5HAbu427gvUhjuR00Txuhx0FKk/8OQyDkwFcUPnGZDITlPtZG8uM6BK0J54OyxjOjKzlzy5E+lNbgOJdhZfMVfJB2HgbFdZKiGwzqiN6RlPcAwalTlLonGcyc6jR+6qMfEeNMljkDBkKm7Z67AJ9k6MkshsFnQBwHpIhl8eGpaZzrxn85Xc97pDtwLtJYblFSpR5rxJgTn4bhHXutPo/4oHkZEJZ76tFkbglf8X0s3ZC43ORLzpjAkJ60ZD4uKTmZTiyJK9Sj3gCiTZGija3jgD19BBDHoQLBn4/ZgMAaIb2RpO6pjK+TNDTKRAUgDpJZJ0Mo8l7pAnzia2m8+i/jdXHpmqkpyKhAtuNxAhDdhc/jSmCJPUWcy2k6stxiyR0TRkQZ477xL1f/wiVwLWTqMiAuNXceB0kaHtD8i4wsKQutLSVjeeX0fNbFSj7JxXMk1EVylYsN9UZPtSk6N/3yng4Xxfhp7MELgNDPZUQcdHU7jsu41J2vgSkQZWoa754OkQYAMe7DiCCgTEOUiW1M2UxNnIKPkeYkezLnzHn2EXOrNCZjAqTBcos1d1x6FLU2uO6wGsiIKEszN8dyl2tzSGQjQm8Gx3K4rCittZLLMj0kbXmiCkTKWhcyIOqaMEsxJAeEfNcRkXKqYzKXxZrLy1pzYiCSzKq5ASI6IPju6axTE3f6WYGwqYkHLLVTE4DgY5wvBrQA0WNi4HmaG3Q+j3syphOdmnhKKhOfAsEN7qUb+jI1lbsOXPVjc3rLvU5NvEYErRtl6foxL14DrORAfmpCDlzyeJxLTTpNRgWit+k2zFKkyE3n9uqFo5OtTIi9mxx7d9eUuqyXEGYnil1p9ZGvjQHTh00jvPQAiHFBGaEYZ0GyxTroUq2LrQKBFOMxEpjHT7xujAswFr7xmIy6NO4hXWAHy22EUZdMDKjAeaGM80EXZN6jSzMfHZszptDccYQX3sQr3MC3GpwPlmNcL1ZylK6z9MiBS+aFnLszoKWSK1+EnY7ycQ6YpOgGAUJHRCcLMNeOuxmtA9xj7S5yj5qlZjIiOulzWazTzbvKcvvq7tf+1dvdtHpXml6x9+1bWPllvVJ6bM99W4mvaa/vq0nPtTV3v0pXtyUgXnP+ZwJBrwLitbX93O0Nz5q+UvX/rNptQGxAbEBsQGxAbEBsQGxAbEBsQPxBtduA+BuB2Jrwx9Q5/IIz/htApNUvq1v2Qd7cHutuF1LOwHPaejQt1CBNvq01ylgUnUTDlvO6syPieg2M5fJaIPhZNpgxxwHPZrmO3cAcDysx3wYiTuqXI57nL1/v4RYQNV+NDFjgpXZeWDkzNkD4ehUWBUdU0nJe01aFu4BoiBIpzWo0S+P6IioQNYpd/kuIqsgj8LcDQVeAuDkiZl0xB2LtzLAOBDkg4j0jIt45InyLLHB2HxA0B0KhQEQdgV0C3yII+wBMB2NaGMMCDIYaQuG9ysNQHgUmg5qyUwYFWBMADbyHrvIeiCQFh1g4NQI/2ItUzBhBSfhuuXN2HAwyPkgJ7QwIXwmDQoFYysva1RlrhKy1riQNffF/Fy0T/JGSO/njvg4IdVnoikvG0TkQBw8EAoDgO4DpIOwIZTwkfEoaoowW0+4jQqt9NCB8yqQMCiDOkTLjPciZGkrkcriUI4Kaqdd5pY+aKiF0y9GsXlgcyH2EK42NkgBj7+qFYy7YG+d5kXJGSKeGCI6J1lRbFhE0PQ5gj5y0TPBH+tjQjfS4rwM5zorRLcCFYU6MMGMIeXMY+ihknmR8C/AdwHQQrpOxIoTdYEAYXwEdiRh3AcKlTGTxYhsRxnvoJO8EDoZcBFwigLCYsNREmBaFzICzIlgfGBFcEFgXVi+EKh39IS7lpZyR8W8nZUcLY2Jcl7aDp8LskVO0EWHtM5ZLPe7rQI6z0pHxXjqdmoQZw4wO0JPkogAnBZFYYRiA6UDkWBFS/S7riNDUHohQ5taaEkDIiOg9ECnT0Mc6w/ZSCoDocgUCQzdmD4R0Vk5ljeiE+9D7eg1LQMzyEs6I/B0SVSAIE9BgbQdPhf89lbWwXGjWgnLc14G0LZpCeS9kU1Ou7A69UKNNTcpzMKaDDOi+DK3eqhV1MBsQ5zOAAAvEp8TUJCuA0gCM9zBeEQNYGVgN0HEyIgrzo0cVe995uHqMaBPl6uJzeca2etn0Gd1d00JevTFH0OUxhgKzlVR5KvzvqdymT4Gox2sdJBcqrJJYKHlRgUigJbV3TQl8CxlNsTAdeIk7R9JF7WxcBmE5SGqwFJhyKKPAFuuSUu6WB6ms3U4q74Hv78HKsPsk3n/U+2r8ogANkZkWxwH1wXc+285jBniUXwu1XrZY44zj5ZgX8zobc6TDjYKW3YF1IqkrT4X/xd0h7i+5FpXlUo/XOnTaF8FuCs5H7B9ADRVmjLbFfkeEKxSM9pbuarK0kjJ5qthy6tmZ6c56gAH+sVSE9GG5LPfXBzwNuJ/74FMm4TDdC8Q9ZdQ09k5Eoq+4LbflL3jE93c0agNiq/PWqD+9zhsQW6O2Om9AbEBsdd6A2IDY6vzHVd7q8Fde0RsQs82eU2JLxoOIrZZFcmck/b9b4WqQZUD2+MuevVgutTyf58IejmqpHkj4V96Mi/IG2kX+E0YE53WUUFOOIf97OeobbJOKSF2YL7FcW19+dwOIHMPq2f6T46Yky7lbL8ee3NvpscTBvYRC8p1LFvq5xk0KDojozrT3T9u+WgeiRCGSva8vT/Tre/1Jn/PyK6ipvkm6IOrgXkxv9UGik5cIM+WQCgRew8+xuwuIUPfkpbwn/dc+rE5O0+I6EHQXEP0HA1Ejxl5gQb8ne8m998IcpaC+AYImQExrfg2IwwcBQUtAGH8DPAi8UW0cBeF4nI2rINdg6hDgLKyPThgOhQOh7I5DRLiG6QN25CjHju413mPDGbkMxvjozlMgDncBcZCJFayQ+gLyQYGY64NI+TKBsh5JsBRaqwoEwkF1atKwU8t4GaDdAeUQ9BZ/ughJwPIGEKgD2CwsVIA4s1xHvXImoDFgHAXheKSjvKfM8VbWJAhKEIglUCgUEGXrGKkgnRHAjKJf0Wt4UkeEBAqPAlTvOCMn8Cg4eNvLGiHcB52azkeJa8l/gx0TXkSuQKQz6myhUX0B/awjYqYPEslepj9ZDnIEtZI6jP/ZiDhJLlxvDcQaP6l+Sr0qh6C3AiLbLm8bHwi99vLm/KWE2aPxKtCtoXAU8N68hAovHR25GrpGgPURwWtIpqQhLA0A0RsQJ610V6Ym5laUqUk0M46iqXECj0Jyenk5ULNG9HVEHMiO4XvM/Dp50Hizj3dDLYTrIiNipg8SVcPDgAAjA2fOR8SpEiZip/QE1D55pQIAIb3FeXOrat4VCAgLxIbvoEDooE8I6J+UWgCBA2Y7NCPCgupkQCSqcjhU5CEqEMJ10LxMfCKSCZZx+cb44G/j1AQ9EL9Y29SUkaYCUUZEbIFIBOYGgJjrg0TV8CgjQpVBUKvpGlGB6Lk3IIJRJvkpENxbY3uHU0wubwMCaiQydiWj1ABRBIEKhyH3IGeCS5GgJhFKY/WcVJUoTOmiABGZx2GkLgFCdDwUiKpe0QsQfVT9C+YvsR4I6QrQ3DUl+ZSuABGbqcmAmOmDxJjmU1NpxxQIZq/YGgFSg9PeKECUqSnhQuZW1bxFI0svBqzMrF5xAZfAgACTQVUtwGFIB+NxnC0d5k0wI1SR4gwlCjAUoHQBLQzmN/CyhCNBVDtCJVCOny0d55GFRyFpzxDSkWPZ/45IOoHa7wj+L6gOEmpouiJ1sQZPZUkfJJh4leiRHAsjQ+qQp78jeLUsvxVYn6TW3j6dmeijtzbSfx3frJxd3gQGJNRIEt1H4nC/CF6zhQ/IUxv/6rI/bgtXflmnN/WLnbn27cb2GSoWt/P82o840gf1S/r9jfyzgfgPbRsQGxAbEBsQGxAbEBsQGxAbEBsQGxD/FSA+p8qf1RG/voN/WYm/AojwSfl+nf4B7+JqkrRyHj97HEP4+sDY+Bf8F08lj3AVGaqnCNLwu3CZRdAbH5Vukj1HytyO3Hq32DMf5Md5pJpLpPLgEE9+y1lEk9py/ULOEeHjTt4EPA6mnoGn1OyOAqUPDgFLUEgihdfqv9qSdBWI+fUWFoGIEyBSzPIEn/0fXEQ5+oAJPoXm9VsGIpH5qIRJiUECOfbeXAn8N7SWaqFhL+smPTc0QCAW4ls1qa1IdecaYSja+zXqIO4oCQSBjL5HKOhK/W3PrCV5AYMwAeK2+HILRF+A4PhAE9qfhk7aaJvQYpL5uuQ59G31l6xyWiBCasRQpkBQC8SkttIKc4BQIAo7JPlWIOpM1rJr9V9uiQOiKwwMDoIgbCIR3AFZgZdx8C/eFqcRDNhL0a44SBNYF2AyIu4EwnxUOroIC0I6lmvGofoMSXMpM6MZ5pgCpgg20f8Yr1lT3oDOhxN+z1SE1PFC89L4zcIEmQJxuAHE1fpTYZa4lpgKymGwbFN0gUTrJmMqxIhpwLMWqgvKCd978CROAoTOupVjcRFHEf1fomeEGN8xi3IRx72y+agEOkmoEQ3hT1FkH46qC4Aj5t1C5MyexjryCoFgpb16L23qKxCqhKHtbGuLOOAY2DzDDQVA4F3oss/agvB/Rn62RqzUnwqzxLXEVFAiVCI6grIFVWwJTiTmgBL1c2UtVBeUU4TiBUAEEHJt0nxEHCzQrp4iB3kp3uQnqMzqp0IGQ6wa1T84Moo5VYA/4Qa/XJkSzRbVDPNVSc3UpIwLYVW0tSXhoJxwYVUiRDR2CDukiDuKjggzI6muAyv1p0JoaFoSzGMmkahSJGO7gToQlMtnDihRPzvWAg824TrIiCgL3amE241/0UxNuQIBNsgcCF7s/BWFhsjMmudA2G2KB8I62smzUAtEgj+MTDptbSmCu5LiAhB1es3rQCzXvwIxaUlWj5kyNVGdmrpzFSWAA0rU9aKyFqoLyon1NYp2BXMctGnGv/B3TUmBEE8RTbMIhDDlcmmIwIt7k+p0BwEem5pC1cQR8k2aTE3K1s3GOunONjU1tRUgzufVu6ZenVJSA0SaAjGtP3gbqH1tCadWjxmVkIL+hC7WFE0CyhxQoqwXnrVQXVD4Bk69SMRYzITejH/hf0dAjcM8RapnC08h5oRHSfSd+O6dwIhgNYtItsSFsljDu4VMNSTLf5IbJbvrF+aELNY4fiyLNbfkqH4xtbbdoNIl2jfgnbvfEeeqxsdHUG9Z6eha/cHbgAtLbYlgSObV0t6+0lRRo/28xFpIk+/ul+Ob+Rdplm/Ntb2tNqr9bX56mAjCfVxtb9c/LfcPXaUOtIoa7ecl1sIVIN7VkI/PdamVn/OII105djcQ7922h35fpMQNiA2Ir9wtGxBfpFs2IL5It2xAfJFu2YD4It2yAfFFumUD4ot0y28s8b9c7T++7l+gMZ/DEPm4XHmL4EB09atsSf6yGoa86hrBieCQ4/Hy2vyhFlzzfXn595IROyt5pVnjUrN/fAJ6GQOX/FR3mD3bXAt3yyYtyx1NS1phZcTqCLMChIobJHkAf9XeJFyp41LBcEDx9IJYgNAH9cU3JN9yy17I3wdZAESQt/1tb3DaGd0aEJnshVEnJN+0ora+oSSERYCxL8xHTpyySWZAJKSLC2BWYYy42BOLALVNaCVEKhBQg5ewjASEhv4tQPTLQJROqdoZh3UgkgaIbgBBE8mOa0Cs9cdVIDLShUl9Z0BUfsbBHEYGs4c3lY2ueqGcwZ6AY8r4LcOu/qAvx8IfASYIGukqufDfHM3VBCofh+KwYgEqiWwLEDmi3BYI1oaBdsZhgMqFQjJ4T5igdSC8Ep/NK4XL71RXJONVXAn1mLNKNxgQxmw5aP7gg1jdbbJCfwjDo6Q0ZZLLgLqzcDv3lOVbfVuqg8z4yfgZpDoa0MgwDkcspkT6CngP9kRQfQ6QqUxLgyO9/RitEoaGsDNOEsmKyv0g4wsVJ5K+vFiu/yHgeuazMjuhZA5lBnhVEPYG1c7o9bV2Y5tEjSVH2c/vTo/hSlwi2bxScmGcQB2ENJDaq7OKXblBekH9WMh4J/YKv/nEQJJC/o19SWnKJCexKsDUOKZLyBcEBfR3dZApA6v6iEAjwzgche8gXivmYFKiwwk4m5bGCfyG8buxM05qJ8WsDy4DQfnqRKJKB8olUS6IahKwesVRWBYGBPYGlWxAN9qsi5yOqg4iHWmGOBgRpXxrxakoFKDdQVxhyoi4IA5tcbxE5g2WSpmhRMu9e4y2NnFvyIig6jyjo+qC/i7GJRWIvviIQCMjUfEQwV7xWkmTMH2Xy2Id0UnqGFLYGVmBGP+KlwoaX51IegUigUtCyS3Wltt1INQJxSBVTxherDHuilxWKd9aUYFIZECkMiIwCywDYWU6IHoPRBInFb4IZY0YgssXqiDo7wUg2OckmgZFX4s3bQ0VENFBXUaELdbRPFbgGJILO6MAEeGl4tgRovJxGCItTE1y1phWnEcYCLi3YG9w2hmVbaKQmlNRVoZqrFIoVr62Qpka1onqrNIXIPrK1EV3tVOTpUJ/gNvS9JlOTaaqEx10qUd/VweZ6PgZZ+hoQCPDliNV2VCvFVuswdFjJgIUN0xLA1U/XnJhZ+B3hFIcubqFHQGVj1RKqIu1/Y4YF15xHmEgWP8CvyP4U1DOIOS3bIk0HRA4fIx1zparurJo+SgF6iCSA0imepuCBVT2SunWveCDtIv1sfQHNVOTtU8YIuCKnJOdMQizQ/q7OsgUfnp7k3mvvkSKr/m9mO7ct3AL+CFbos/b1npsre5prU7tjnv1JV6nQ5He2Dl/AhBrPXELiD9q2x76bY35T9d9A2JrzFb3DYitMVvdNyC2xmx134DYKr/VfQPivi3Nd0V/JK2lon/l+eq/l9bBJC0Xs8CbmPuqYL8xH7p760uoBTM98L1V6cBJyZXgc777oWWspaVa9zgFwspOevRuVZM0r9QMiLjY/EBFijxO+RzTlEtApHQdiLAo9ZBKnnU/apGrk0qcNC3HBgifc1jvcZpTDFCaedCEBSBibIGYc2Q82aDUIC2OiHA/EEUUna4AsTwiFh4dR5WAboRZloHw+1GLoU9x6YwlIOi9QERaGREN0ystAzG79HkDq4IZC+ySAh8Tec13OBT/lONgfAbPxWiBAKuieqM0nI1icK9skDMaBJ8RHLNPLIrO6Q5Q0RjM9aTyTMRcRvQtioOJ1AIRt05DRGMByh3hmpZ6LeRsAuuX6vhSND5arRHkZkAoO4Mqd4QDQbFwVvgC6NRXxXxn9EVhKuyQrD03mGuPRBaDmgUgJKjKG5nfZgafAW4rxsVogQCronqjWDrwH2DoIewzCcbqiBCfEUTtTjBLwOvgPRQswKsQUsDQq5UAl5JhO4AX7iOpUsYpQmSk1+gdXnpHTUu9ZjmT00zoI6keQFS9jVZrhGLfOG4UY5GkL6+LS1jhrJQX5an4zoDyZko3xwFjpjc6ALQ4AISpVkBxA44gsahZeC7GFAgE0hHmbDkbgSz4CRcTLUFSGGfkFKuDCZoWlFcBvQ0oViR1eTAdjqjCWAAikcksqFlAqZvVaynnTlvLNYDbCpEdabVGuktyQHRUFHYSuCPdhUeEcVbMzQs+MeibKKW66Txrz+lsJ3oBMUPQoAIBPoaxNJCycjHWgJDZseFsWOC/ywjuGxDaeOGMqOMKGRB63ZK5nkCxogVCOm0wIPIMCBq0wbl4tizkDJGtZFd+8YihRa0Rt0YMBkRU7gh4TMriUBUSK8mAUL6IAhGoAULdUADEOAhhwWHUskoz81yI+V1Tit4bxXM2ytTEY4LHISa/xEwO44xMp6bDYH5BmH6gWGFsEQEipjK5zIEIOjUpELEoacxyrlMT11ntq2hRa6QvQCQ/NYHjwscVCC0tm4ZHX4CQUisQsQUCWhyB4PAxLpm4W47mCAKWhiraFS5GION3gA1RU7bpOlXDQDnCFpHbgWMWpoNyRnL0izXzHNR/RF1PkvJIsNYwcwI8b9UEoW4AEJjZOR+58dDVotRrIWdbrEVT5AxGRcm30Rrpztoe9aA56hIeVE2DHWBikeViIDotyXxngpRa3SOtxind+pnxqmO30qW4cNs2uY0t2jZvLNfdXKfXtiC9s81v/2Wd0utPeUe6Za7DdO/HAEFknl5/AhBrdf2t2/bQb2vMf7ruGxBbY7a6b0BsjdnqvgGxNWar+wbEVvlfW/e/YiRsQKxv11kZ08Cr89aYqnTMlThw3PQrEq09W6KFYPWcEZGufHNR5vlRl7evy0Jesf3cllCBaNxebmzpjn4vegiq5dGcFO8Aoup2o0jvK1L/RntHk+INIAJVw4tAU3uMtwERGhKBr0s9cQ5EuApEqecVNZO4CkT8fCBo0nnT4/GOEeGBoA8Agib0jFqXa0BQ4besAxHeDkTLk8BrtNCqQLjH+Aym03EYzJ/EVDQOheEgr81qjnjTPiF6ncF54BRB/U/gdMJ5vryY6oZxNSw3KQEuJGqmwd8vg/EprHae4TGv1VSnw7FKhHni62L9IS8XD/Yq8AFBMTJ+Sy3BA3FQr5ei8qG14phg1d5o2wpei5SYSsAxFvsCvMadGlYG9CqE29HbK+P6MnxhOPC/wd6kFyBYlyMmsEKg3iFEggF6Hhw35RFhqhvl5W8tp484ywKK8HE4GUuk1M4zPNpaITev0+FZJWB8+LoUHQCCwwNa2hfVjvrKO2rogeijjYjs+gwMmaJzQrWt0DZRAwMSrQ1wIaLGrk2rwrMyklZQTJGKmgV0OpCyu9TYdUcGhGgTZCzSkFUI6n+CxicBwlQ3jKtBWo7FxBUIUcgwIJraOYZHWyvt1Eano7JKgpO0MM2CTlMdYcdzqaQBUznoTJ8kpmZqMosRE5cwZwzhA6jOSVUY0ZoKA6QjZRqAC2FAJGqBSFSBKJlHp9NRFTSsuoNfrAEEOA+s3pHIGt9lA0J1npSrYeX1DggW/MGoaIGoZy3XqgJRdDoKq8QD0SkBgBVBdESQERtSGeukKV4PhHE5alsjXF04SjqYr1ptVoqxTjyelWHpUp2abC8Zw8FNTeVOxE9NRr8pV6FMHS8vprphXA1T6zgMCoSMaUwpDRBaO8/wmNRKTm11OvzUNK2L9QfkLERhQ6YmrH4TfotMTdAlUSAimLdOcUSn+1jyrW0V+Zlz8VZRj5JBNSSE2QCtChHPKqwM1ekQc5sioKVKF8ZwsEW+EwcUqbRwFLDEQb3jqGUdlcsQBjZ6ge6FcTWUQ8KKFVBKGkjUNY6ilZFV989pgDiGx6RWklur01FZJUc9o9aFyJxOJOeyWFNUfQ6qizXX8OUFPi3w0YLXi6l8mMUaczhM56QqjChTRFksM/76R25h4VPL2EjG/l65AU13lnRvuqt5xGv7UlwuqY6IaUvv3D7Qw+VVW8vYwLevAcQSw6Tuu6W18eZHHL8LiGuN+RO37aHfBsQGxAbEBsQGxAbEBsQGxAbEBsQGxB8JxLQSv64yv6aktpRrZb6/Pu/KYQPiE4BIze5u5Vu6BYR7A8lpCE8KTSv5la3Ry0hlTz04PvMcnH/0rBSQDMwhG89UzeDAvz4WrjymOo5PVGk3/ifbXlv58pP/PmqaR/n0+BMpfzzLzl3JAp92j5pDs5d8Pg6Ilj7QPkNsTS3iNSCy6+TyAutUUWMORGyffTUyDQZEcLWBGUBqauf9TuRhNf8FE0JiA115qbim6jQgtth2BmKPbmM8Xl52Y18+yKEHTfIgn37sAcReO/5hAsTDKhA1twJE+AQg+ujPfy8QNAVicjH7Oh/k/4M7WQxGGiCCpAhpVXuDgXhC/+3GzroBxJ72BgS9EgjtO33Am+sLvUd9kduULlp3kaMqViwBwUwGdhKBYsUhisJG4XV0hXMxZV0c9Sj0KeBwErQ+wrtgPgd0QoQdAiCg9qHOLubxMpgXjIyQbKEh0neiEfCRsE0OEj0swunGU5E2cDkvL2NHPT0+7/fjpPP08vLjJ3fd4/PjM3cg/i1APNHTfsf7Hv95pG/j3zHFP/iOc/Z7nMN7+dM3OV/OGb/xtF40IThKCi0Ke6Pe2AveXSSWAOUSEL3I9UMNI43RtJPqXiRjgIiqRlSDjT6ql4u+DI4xBIcTfSkc6hd4SVyCigZEJ9oGWbkhZB4vWmN1fZELQjh4KZBJsViaoK8A15g7eBg91EFGIHgk7Hdyqe9tRIzf5Rofd+4AxOOeIXugH8+8b//9gb7vuNv33+07n7N/Ivz7XUbO7rucj9z2Ox4RZCOCDcOgRVE0JpTr4N1FwEiAZsYcCPNNsIA7gABPwfJT15YiqwBvkaPT6GB2A4Aw9Qupk/AdDAgYBZyiOruoo4WkqCOi0AdCDkWcqLNFvAVCL5eg2hoYETsDooyIAsS4SAOIBwZpTPmNfvx8/ImOFSB27jv9kHP4bM6H9/P5418eZdKHg82wY2A/Q4uiqK4UrkMLRHKc1ckaoSPCFmsdEapqYXoY0XFBzFskkdPoIB0RvVEXRPKBV6DB1ggPhESKVRuEhiUgIlUgaEhleQetZQ5E0jVir0DUNaICsd9VIPb8Haml4x9x/ZfvfNSA2JOODT7ynXiis6nJFjSegcBFgPtVqnZIRdciajcG8gwGjdtmTE2qalSBqAwQs7PpU2Vg9CBgRe1SPnYA+eas6he9AhGhLKN3TZE1mio1Jrga2/dCqJFb1TCbmsQKqeGp2GWHu6YnALHbzYGQSceAeOJ1fS8TD6YiwtRUvusYsakJx8ax8E+dmga7NzGGRhAzLlu8p+4iYVCeRK4MBvuXeEk/QxND1Sii6l4MygAZlFNR8mP9i6C6GjxtYLFOWEKjql+co+mECDtEf0dEUEStlsfBFuKjfsciDMIa0yErYZIXa3FIScZXMZ7KUXsBvyOeSJbbZ/sdgYX3hyy3j//8kE8FiJ0sxf/wWiEL8j/2XRbrXbNYjwDwkfFXxjMfX/1JeNtTZXFEzG5Qb29p8Vu6Uosw+yV7rwNMWPm8vL3nl/V+d1+NLN1q7vd4paw/4kh0/5au7L3Xp+R1zi73bu8B4vH5vjIs3fas6ZOA+KUt2oDYgNiA2IDYgPjQhm9AbEBsQGxAbEBsQPwngPjMhv9VKi9/2rYBwdvOgqr8z25fIqscyHj+8fTw9PKTAxpPY1jjxxg+lODG+JzkhzxXHNM8Yc94ZPw0nvHz4enHE3LYlXDtY1vkXh4i08PzwxNH3iUEr3VQNQJ+dptD/lcCUeyNfcxwtS5bhN6HBGMzfxrPGANca24tq9vKq1l1c6yRqM+2CktF3oDTV0rnuh7pWs7JnwUgnioeLRDyRH7sND7yxCGOvQU3LMAhT9vt217CHyQfJkA8aFGWtZBRQIDYc+jFQvUMCj9L13cyE14JzvKqrLE3XAdaNEHIPBLaSstuLWEdA/0zPc2//VeBSI3oRqBYRTEaExSfZ5rkp1uO7qxrQOytqx0Q/H8DxHe+oPc1AmWxqVtAfBv/fpOzFAitw4MHAu9aGxAxzTU1Un2n2bTsl91argwGvJh7VSXZAUETIOheIGgdiG/PxjX4JiGPb5h2wEB4Zm7CGhBlDOy+cac/LQHxU/gMMi0JawFhlUctBVPTHk/lnzhGXDkT6vawAoRpXAghIK8DUd1NKhOECj8E/0N9BCoaVXmEnVSE+zTgMXsqih947di0Ppi6BoH2DsLvZD40U70QUxYxxoh5veAsvmAL92D8wn9JQ3kab/3xjE/j7P/8v3E1kFD4+Jn2HKGSdBzw48+k68WzpBpz+t+Ysw4ti97uOCC/22GE/HjmC8CA2DnOBEaEsEFGIMb5fzjFo8TUef6HbgirYRzxVjSoCRJ1k1TgpiAMClZJYYJoUDiq7gdCvz1GRFUeEbcXUxuQeKXwTeQ1925IqjyiteO4Hgd1JR/40Mz1QrisYA4247G+WIxI6TvHPXj8udOYEa7sR7AYhI+AEfE/XOs/+fPDk0wsFofd1RHB082PnwrEzhZqzmPcK3nuylQl5JMyIipnQkeEMDMwIk641i+JTCVBr/zoR8SBX7K/VNsn9jNpmSDgq4BTEtVXxSL35gfDZwEICd5esDKUtPF4MWclrh0i/igJMfQjzfVCoiiJOHGICkQMWBXk6n36rlcrTxrKNnjeey5CBUKmfe69fZm6PBDGZ/BAcF4yDSkQbmqSNFgjKmdC1wiqU5MCEa8DIRoasQKR9Mp0TBDjq0DBIxZfGecHA4cVWVwHDcXGqkZCdfobEhkQqRqbEEZEo2ai9iItEOr1okAI14Cexini+047zqamJwCxNyD++Ue+fZfFWtZyu2va+7sm3DmNCzCmJiWX2NS0a0fEfgd+BN81Vc5EASIZEOczHFkaIPrqiKJ3TSlRNcbAwBdWSWGC2NRkih063eSJ8khfp6ZCLupjIfkkNzXBTUWO2NSUpnoh4K4E8iodZF4vmJrANRg7gYSvIH1kizWzGHhFkDWCR8pefkf8w58xcsBQ4N8W9XcEjyrZ9/S0E/KhLtbgMzyM/wOIH/pLhCc5/I5oORPwQJRZWKyi5HfEGVwkWTQjFkz/OwL0NPsdoenIM0FssYY2h6mPCDHNKY8wG6XQMc/GcUF+psRhMltlsT6zVoj5qbR6IeCuHPVIZ+5HCaU3ioB7+ujt4VU5f94v6/RBaX7R9vFAvG77vUBs2y8AYts2IDYgtm0DYgNi2zYgNiC2bQNiA2LroK2uv7PO720cJKlVmHoD4mqdjY8whAxV3iBPMvlbnLmFJIK+hXbz0GYrMTU6lueY/CB/zGt82uu5DxN9j0k8OS18asq4TA5E16gE2OUV5GhB7W4lt+slda0iyPQ1M3lrfAwHqY4y/5tj6yGzrGPSfn55qXYnRiaR5/SIgkgjkjquN5YanE2seybmFgi4hGJLr+HzaC/vtgIPLRBe9sEZ3s/6O+lLyf5AMCCy7MAL1LG8uHwFiKhpJuOYvL5HWqiNRhWcVnig3HJMXAcnV9mk/wYFIk4MgUrWBPP38hrvAhDzi3ECRG/5cDSMn7vna0CEhe65BkRIqyMiay3UIuUeIJaP3QaiBxCm7A/zg1cAYSOCdUHIPFJwDGFA6cCs6gGZgeicJ8hBLe4PJu1OeF+6G+z9aq7SIdq1HtO4P4toPOnLuzGUcI4obAiH4qAchxwP6mriWRXM2TgW/Q7OfImhwY1C2IXLlHZJzqGUZeyKrpzN+hvgXVQXFHFdGexdbauR93uB2kgF4uCAqDUL2r/HwhmprbJy+eIR6gI8UgAPj4SDvGtMGW8ckyrBg/PAUS4oauAVdviRcLeYFwrWiLFKEheTWSmxjoCtEQg0hhoHzsah6JXjkCP0OVpWBRxP4IkC9Y0lhobKUMgVGsCsyGQ6BupkYjFsKuoictRGEq5zdW8xIKCPYG0k01Y4M2cEiv6J31DnKGIm1QSJGmrt5Rrso2mbWKusXF7XgjmhqLYShyUzCsFb+iRv4B/U4SQWvgKC7sUbJVcvlIO5RPTpgnul8X8O3Oci8BAt+NldOnUuwRE0+6R8iYmfS0KJ0S0Bc4aGAKFMC8I0m6t3xVHmYiXb8LnFPwK8C9mLYKu6t9Qaoc3qyWI+K/0BVxrmjljICUwOvcAHBq4pUUO4nWuVlasjwgWyjdPQACFrRE5FM0NJjcp+qEAUL5TsF2v0sAyZ5IDoQQcQXYxsHIreAWEKGs7PJaFEVFQ1NGYMDWhGgWlBOklWIMarfjAglKxWgNARMd4G9DLdwL0FqiSnwvhI1gM2TnMBIlYgkIMZ8rCwRbTShtoqKxeCAcY+gAqGXGopGr2TzxUgEs+5CWwshaRYRKnqUfFCSQWIXhe5cU/WOzCU152NHWFcjECYyRM8RyKclxpWRUqq2GG3EksMDRVmMZZGjzLL1JSqY1CVXDH9DXNLOSipjWsFVRKpkaqJGGOjMkfqnZMBgRzMokrgi/WCj7Etl++agjmhdOJOKDzqi0hPZYjrMJZYrOFaEtxiHcm8UVRTY0AnMV9b1xeyXyNZ/ooWBi+g0dgR5jlS3VfMVEZ+zTSsCji6wBMFQMwZGjzfCg0y4xxzVDFuhXE4SNkY4Fno7wBdrJMJfZ2PqkoiNRpC9WQ5G0tDaGVyBn5HKN9EVUPAAqnnovxQ/sVR/h3hfqkt3b6VW0K6HYC/dbz5nRCXjtyTw61y3/PLOi3meK/Ox7W+uJ7HKx9xpFcWfm1b1s54TQ6fAcRyjm/R+Uh3tfeNQPwZ2/bQbwNiA+JPresGxAbEBsQGxAbEBsQGxAbEBsTfXuc3Vnoj52xA3L8Z62BKd4lV88B5kPB2DyeiZjPd39E0V582XHF2TtMuneTQzY+ohHhrcvzvJS3WcvqZn5H+e5m0N1UWSvDp43J902rfyCvG4q3Nz6odL2VCP2B9+dyIG5CJHayF4iOludZEnAbdw6TB7gx+nh9jXO7nuAjEkrZxaIDgdvA7qty+To8vAREWgNAIim9vruSHdSAqLyQ1tW9JE4j9BASh3MPfiWsJrAleCwS9Gohp2jV+SLw5IsLiiOB2GBCHK0DMP/fvAIJWgPDjJVkkEDUCo0A4CXAoYY5BxpXUScBCVSaU8eA5EZURwTEpYSeov4kMX9WrqGoUUK7gYDxehZUwTVG74FdsYUpfGSNVFQO5jPHzFXUNi++xxwo8WTo4rciIwGUVFAhfn1lOZ5NxPygQvr1hwkKpGhw+v8oLOQzJMUDADVkBoo8WS4ZDCaT7Mbe6V7kL48FzIoqTUGE5mL+JRJxUr6KwFjSuK/ImEVYIXu2CQ5PQvI/qbuJVMZDLeG2vqGtYxDtLkFO5GL2tERgRBoSvzyynYqiQzpNAa1YLh8JCsfQkmiDG3eD3ro0XYtyNSMYNMZUQ6IrwhXnMAfoinS4eHCw8KU+hmYoQECQjA8z1JrqLmdcE99Z/d2nVKMC64KadoGWR1K+FoHaBd/jB1ODjEpQsqhjIheO7S+oatZGw9pCX3osix3RE+Pos5QRfFot4+/aGloVCVYPDczcqL8Qi1VHdXoIjcTJXhOoaQZEGW5poOClPYQpEYtWJCRBVb6LKI5i/CVG1zKishaTsEKhqdBJor2oXBkTSlFKPooqBXNRva66uoeF+A6JyNABEOyJ8fZZySmRsjwkHhEBNMBaKpiITeTDuRuWFcFojWyT4wTRTkwOiFgeHElSqAOEGK4az50QUIISZEYskiBbV6+TgARG+Bnga6CindtErEOrZEhtVDOSiXioL6hopGuMi1Br3YQJEKFf1qk5HnZrilAMiQHgWSqwaHJ67EcvMwGkPQ/WTCeXisEulAAF3FBQLUiJ7lui9rtzV2/IFxoPnRBRGxFlm/wEMQFuOTCvDWAtgXfD8qDRLIx4OqnZx5tnV+CHM2fCqGMgFflvL6hrGuDgOLTuk3msdcRtCvj6znMpiLewLattLqWGhDFWDw3M3jJ+hfEIyFkenLi8GBJjkSXQIpzd+i9/v2VKcf7r3jPs369J0Z46hOWupZeldbb2dU4pvKeEdW2Uq3Mt7eAs/4joQ9zqwvBeI17BQpp6qf8m2PfTbgNi2DYgNiG3bgNiA2LYNiA2IbduA+Ksq/mdf8Vdqz6/Xzbe3PIF6yxZenXK5vh/ZgvCJeSsQHBtKda88Hrb393lzx/I0Vl0/OQbFgodGSTXgDT38O39ypmf6GsVJ/onZD2CcRFKVAX2X9MaWavrXbZL+ev6Wd3q9egiJFof2WwtE6xedHN4tEF7KwF0P2Z8xq28sD4cb2YdWDyPOgAgudyP6VHkFru+01BrTj81ldMvjZr7FWT/MRoHlnV+vHlKBiPcAQa8GYmnw8mu5CkSaAoGcwwyI4BkTknt5Pb/kW4GgO4B47SSypuNBs7niGhBr6iENEIciHQ71Cm7YQdQvWicQPAA2toIxEzjUE6iqT9Qz9JOdw1SUzLEs+TdCJaPlPSCQcyxncminY+kizT9oHOwyQGUDzUd9az1NywOBmKOEuwLV9MowGVhjo+pvpHQpehldqTnpeVNHFONkHIaad3iDeggDIe4qGm0tYckU0bAo6hetEwjV1MIE009k4Ud4hNQz+hjdS94B2vAco0uU0ZGmbmG8BxsROBMOJcGixlFZIpGj2ABSGBHC5PP17MuL8Yjuwvqgplf2SczgcJj+RjpFUxgxRQ5LP3dEscCnsp8UiNerhzAQkrd7F18ZDApEtYEpTiCetxCVV9Fd3Bv2GoO2M0rQnKBIxKocyr4r+jdT3oNOTbFyLMRgTIhikCoBEB21I6JxLDHNDIJjynjl0WREcCD0cnLt5usTHBbT60hGR81Ebf7CLZFa4UwPxGvVQ2RqirHYYPTVlQRzrgfCBb0H4y1EiuWTHxE86dgZvQMCKhQAIupVXhU8vB4GOZ8TK1WO8tWYDQhhRPg1wjuWGBdl0JzSWHq7RggLyQNBg4wIralKYvk1wvfDIIvvoDSEoVkjXqkeUoFI3B0S/C7qFR4I7wTi3EtEyQckLpPDEY2ksTL1jMMQi+cI+AoqyKXfq4KH8R50arKx1LdTE2swJZntTu6uC3dNtdTgOCBR6ictae6aRHGjP5+9v4lOTVPPE11Xp/kXG5vo8s6vVw/pskxN+B1xGMgt1lGBgOKEdwJRIzDhR7AuB2iNAYvtoNHZVM9IYEoMrOjBf/nXg9iMXaIpd1DLe8BiHQQepWXKYl3y52V3/B2RY/0dIr8jXD27DPUL0SiU+sktSHK3khJJHtuXqr8J2CKmMKJ7hnq/5PO3xdqUNipH5vXqIZ0s1ivMi3r7+u/lNTd6aeW7/xVy/YywcPMYVlPP6+vzuF73RG/d3vuEIU2+NbVveQlvfXaTXt3Y9K7831PfRL9rS/fXfnvo90VqvwGxAbEBsQGxAbEBsQGxAbEBsQGxAbEB8ffV/rMr/2U7J7xy/98DRHh1n7ytJ1c25lmEwYXN80KiSDXqRROfkIj34/695Hti9atpOji2uOPIcw5EfUnQ0tX31eCMsqohsqAdgPwlKJEXK1Y4IW5X7ewBpbYyCkp6yG1J6TpGCIGGSQ3DktIBVWmIutOeyrPHyLWSpmof9sVKCuZfQDVAw3lqRzUlaoSs5FzCSvoEd9XOYxUII+6sUxHCYpdCMiBMgCi5xSYyQleHj/AsFh5f3wtEX4BIV0uiFSCoAjE5jjznQPSm9++BoLcBYTwWvCZ/DYjla9vq3gJRcpv50+gktMq3sMEnf8VVRF/GNYZFveKOZe+x6FjoK+x5znrQcgeJ/g6N/oXxLzTUBCCE86AMCuT58sIsCeZM8F/O78CBHKquJwYEcmk1NbyvypKaRpf9iLjGCXFKIkPlaESCH0vN01RB5Jyz9UPleMjrxst8C8oH4VkYiyGLRodaFSjDogIRqfiIkOlYyHvaVF+mr6yHOnm1ah+Vf4GSRGphkMBiYVAgT47zqk+JmcGcEZLti5fJUSKG0P9oNTWqrwo5NY3qhBLUaQyz+jVOiFMSQcui1dGUEKyvqgIWAq5wjUEt5Mw1vkVAqL6MiBPC3eRVLFogwGhQFgQcFnRSmLIecC05IKr+hQOCz7LjjqFBHohUgeiR2hgcoBvIEn0J1GpqVF8VaIDM1TQait0VTohTEnH5RGGdBFUvqWyUOrmCRQIyAUpY5VsYz8LWCAMikbERwP1ogWDehHct0UDnlPWAfXGq9tECwWdVIApDg1ZGREw2jqgFIsWw7qsSl9U0WiDWOSFOSUTzKa5FFIr5W71kDQiwSCoQiVb5FrEAgTzOZwHCqVikXma+cm+jOizRM6XmU5OMieymppn+RdT9Vh9wHoxJ0gDBXIwUi5aInVVNZUj1P1Z9VfolNY0wBWKVE+KURFT94+AuY4gkGRul5Kc9LnnV3lrjWxjPorgSJuEdDFXFYoSBB3c2zQ7wHkLRsXBT04T10JXFulX7UP0L3Y8zsFhXJokBAf4IczGCaomIo0lxPTmKzwmp/gduJhZ8Vc5Lahq2WBsQ65wQpySiTBDMGEf1ZKl9Zfom0BzprOaq2FE4A/VGr/5d2yo7IsU36nbQe7dWCuKjft+nye3rcqvf1rawmq7J+W1d8xY1jY/ZrmtyvBeI9zxCSR+c7otv/9mHfhsQ27YBsQGxbRsQGxC/Jv8NiC+S/wbEF8l/A+KL5L9V+8+p71ue21zXwrj1ovntF9HDK2sY3ljft+rffg6XJVPR00jNAQ6aWOBkdoVl+eB8G/RcYzsY4yNOGBvT4/Ntzpeo2gYWHZvyM/ItIFZS6F6fT1OW20qJrn5tLcLQ+RNWnVdmx+OsqWmSNElHzjUtylA3tkUFIgeaQBSbrKfH57W7FqYvQNwh06DX7jTss9QTaa2sJic96upXayHGD43IxYcBEUw/Jd0CgpaASBMgiJaPfzIQ9IuAwAmNisgtIKb8BGR4UHWKyjIYAxugE2TT0DiK2gR8TjSNeaQoe6EG2ZXzICESeRt7MG2NyXHhk3gtC6cYUNxfDnoUIZ0lj5eaunIrsvI8AMRSWegPtNu4JoehurPYOXzMSix8juLpIikkrGYmDmvOKzW/bqApPwFAKKsjq1KGhCHDwHSCINwOfSU94aV3vHCuSh6EEKHEZFMNsuvk1ie1SjFtDX8c+/pGy8JpaBT3l16PmjHCgscLWWpjZogsQKxT01JZeslKu6vPDKEv+npOKq/4i3pItpRBtUrw+r5xOajhilTnFZ9fwkzh+QnoFvUAIVPK6HRakfk/k/qegEfRlYZBh6LqYYRSTTTSnFe43p35M7jjqSh6VAkFPyKSo+KkhPIKka3xeKmpKzPjFLuLA2KhLNSkuxwJnBIqbAstx2LuZLypI1UgqgAFWByRioLJClcklV5LNOcnYI0AqyObUobsVyBkIQK9QICgoQKRqAKRWASlUT1LsQJBgxdPSKtA+Dl4DkQiD0T1eKmpKzPjVMgI14FAuyvXxLEt7JzBgEi1hcXTRQVPQCHocWyZK5JKryWiGT9BgFB6jV6TsSpc6GCD/I0yi2IBwk1NkjpZNXFR88SUVG+oVbHIDqhWy2IJiN6AmE5NqfCVCiTV5+R8Fs+WGNwInpQl9zqwoXKQ9+YFU1Q7YqBSYgMErwGYUNFzIA0sc0WcCgiXNuMnSK4HdTUBpwHMDmU4DDpZyTkyNemIsMUa7AXxZ4nGdkBHC5vhjCVCPUra47KviniJq4qjBBnb4aBH62I983ihyiQxn5OxzLOwQ7It1rOySO7+z1WHQ/rgrEyMbOcIz2VoW1A9XbjvjlImfxfGynnZecXlx6Xd4ifcZnb420Hkdj3Pfy/XqcmvUwDxqe1zmt9+uvbM63vPFlY+NzfWcfo9rd98f/z2+mc3leXz2Vv6kPre3a7h+ve1Gv02IH7v9tc+5N6A2Br2n6jvBsQGxFbfDYgNiK2+GxAbEFt9v0CVf213/OGj4D5+xnu75j5OxX8UiGTVz1cS5Vs5VD2KpHsr0+Gq3sWC1sVcurY+y+FQSxggj15KcYH44/jE9d9LWs1rssXVfbFbqUnd8Ex1rNH4TDhXd28OHI/PbI+5Pn9KPod4Cwh3fcaroyMu5BCKxkMib/lRgah6F0QLHiVNP8ybX2WmE9Tfy1t1oWVEZB7bcyBWOFVXgJjTFOLkDUQL9nBMIjdhKg7glJiPOy++BgiiW6YwbwOi6l20Y+31QCTn8RMmI2INiDubcicQpS4chxYghl57oUbjYqJFIExFo2FsGHPB8zOoakdUBoQNRVPVmOYkMaqBVCOjeq9wPMKPiDWPksp48OyKzqleYOPajFlmq18QLgnCRhUI8C8upX1gW1yGVlPDAlwtk8VYGLX+9RXlwhchBH5SPCgQ5/MyEOiN6icDmfwSeoxVZ0O5BeqtIoYCSf1BPAPCBl2jx6E59RrFxSvyfeO9wtFlr3ex5lESi76FZ1dYDSsQXJuDvGXdaww7kAVSKxDgX5yiel0o7+IUW00NhGCZbRG1Nb1jYdT6W8vNRSWqFo90+fmYWQ3kpLpVxSZF3/KO0hvVT6aPSqc5XtjdA3URtgFYB2pgwEeDcD1a9oEtv17MoeZkHdth8EXnvSJ8Cz/trXmUVCULz66wGjaKO9K12Hu8IKZ8rBoYmJqEZXIS1gTXB6oYAMJraqgrisbtj4V7gTOs/tZyc1GBR4rm0POIOHDe+i56unCqAyW1MhHWTPGTSQZEEncP3WmMAwI/g/d0PGcPccI+0Ea2qholp0IwGZQbUUcEYUS4iXXFo6R6kHh2hdWwBSIVIKBOYGk8EDwtnIQ1YSOQBgDRaGpIOnUIyqlhYdT6W8urikkAaaE406ScKxCxKOPE4hlT/GTUq0k6Kau3CSk7ok5NZPcAccI+8GyJMMsp+alp4r3CI2yqd7HkUVI9SDy7wmqIxhdGheNl1KkJxwsQZWoy9xg/NRVNjd4sTNCaFCsLw+pfW14Elgi3A+6uKbm7pl7ZIknB66tuSDQWh6hNOH4GL0zgFqi3iupohAn7APfrXlXD5wR1DCx8opHhvVdiq3ex5lFSPUg8u8JqSGCX5OpxYnuPWrI6suSjAsFnC/VS6sNzNpugtZoatlhDnyQU1Q6wMEr9S8vNRSVaLugdVgOJ7nfEGX2G3xFRlDhMNyQM6ZbyYZFacAk97yHRW7fwit+66Wo+gcIdx1//yzq94ejNDn3rZtX/DNWNjwHi40t7T6mfpk6yPfT7ItsGxAbEH13aBsQXKW0D4ouUtgHxRUrbgPgipW1AfJHSNiC+SGlb1f/Iut+o7X2NCe84ugExr21cOpzct+T+Oq+U5JQxJvkI3yFPvpM33ri+dSv5Lm1t7ZaBSO2nVzi9dLMD49PdCzuB4727azVJs1Z1s/wbIMIqEGGhgOKVkid8iAX/lOi+O/rAqtNKXHprL073xOavr124AURogGgMQ6YoxKX6ofZJonExtq4zcdJPsbE3QevDdSCuj4gwaWrxSslEayNiHQhIptP1y3ABCLoLCKLXjIi42gFVUGQuUzSNEH4AEIWjoMEZsBb4cS5bwJuTSavOEYpXCpxKqt+Jya2ztkXQh8IcLrkM+C6hk8zRrUNcclrR4BDBtaQoW3BQKKEMFURv3FQqj8JqwnVHPS6ar2h3KHOjamsc1AGm8YpBWcw5yYehamwU5YzsgeiK40pbE6Q+DuYzY4ongUJ1YNE6sOQ8kXIUSrgxmfXAywvzGOBkUjgNos5xHMwrBdd29TspQdZkyhQId56guwG/FQmDJnkXe+a0orYBcC0xZYukYVLhYSSiqXdELLobVpOXl6j1OCl3QrQ71C+lL9oavTrAeK8YC8maTUNfpB3AXQlYI3LSfrAAsnBSnAIIa3hE+ySjKBLpq/Ik/S4ck8i1FTaBRXk7FUMQywJMTcXJpFXnCMWQIGgBxe9EGRLoNlDHmIkB+QWoWqh2Rb/ktMLsDABxvFRlC71AkK4AkRwQlXeBmoxASN4MBB/pBNrOhBmKhkdyQHRaV/RBJ4pG+B5aAQieBchGBM50U5PWhDkfUb93zpElFMaIebpwbbuLMypjJgVYC9DeABCohgEBdY4KhKwR3u/EGBIYEWWePOl1Fau2Etdt5rQCXkMQJoip3LRA6MQwGRHgUdSaMBCct4wIkpI48WBAJJoDoWwTkhFROCsJAhN9C0TKHggaPBCprCFRv6vOxxDN3WUg8gIU3NPmBlSmppitOTI1FScTr85BDRA+VZ2abGbFcD9F+d6r4JACMeNuyPliKpVN2UK9XnqbrLSZxU2FgYnFKspqIkCUqYnITU3kpyabhr1XDK8ROJLl+0H9s6y2cikkvWvSSS3a0u/1PYRT1xf+ChRPArmeUo2OEYieYGJm7ATjYHSiWCGKHOpk4tU5HBBJj2gqW6yZeWHOiEGYEvL9HNSxhMry13A35PwgM68scDIhFE+S4VjcUJybClUeRa0JA4F65HIGls2oC6q5oITCQTGvGILGx7gny/dEqElRziBMPfw7gnODLoneUWlNoEzCHBmUVRVPzDESWiL4d5yazmv3kK9iWdh2jd+RJjeA92wpLuQT2xzT4pnv+mUd5zVvzJroo7cPecSx2ok3vt+zgV0033c7x48G4nO9Yz4ViN+7/Qcf+m1AbEBsQGxAbEBsQGxAbEBsQPxJjdmA2ID4j9X2L6j4X6eK858HolvgMyzu1Tc7ERjKkTkfUEYujwXX3RQWnvQsNCitnBev5BfbD156gtU6OqnlauPTtX5Z2eJ1IFKbcXRPQ+uZYSHHMNOuiLO9Gh2VuBWKyFVJO95hazFxbFnqcS/Vci3HsAKEMi+SP1CDs/581zd5Wo/g+mUFAfdPNRCZGoakJdjiVWjnrw3OeAkFiCDhxLcAQa8C4i0jYkroad4KXzl5WYlnDkR4KxCVNYHPCJ9cHGOishHAQBC2Bln6TmkEwvDQPRLmyKZ9MQVizV9EVUDOVnLlVRR+htYFuhrKKrF6LjmWEFxXVHfjbAoj5vhQc/QjwnFZuA72Em/Jl8NclvtRWR52DmlfHC08FqtfTSdsDc8/MaYJxzOpsiY4shVV/eLkGRPCdYhFrD/WF8QLLwE8iRLZZnZCqtoXkuJSRoRTtvCOJ72+hm4lRyp8CONyaF2gq2EaIVrPmWMJx/ngutJDW6Q3hZFqvYGUwsHIRneoXJZgEe4STIWGRxbmSzBtAgCoAkACKiLhqulR/GokNu2j7SJawTwAcUYorAnwDKB+cYqFMaEhdgt8H1VRQ5gIhZdAqteRyNxHhDQjYcXTdESs+YvIp1RKjoUXgqYxs4KPAwhTyqjMjnmOgcx1BZeB/C08EjBIjpc4WSOklAS2CrxROl0vhBcSu8upmNtEZbVI2Ql+MqSqJYWnocobx4sBYfwT0QOhIyyFKmuChGcA9QsAUZUlKhDJaJMDeV4C789BeQ+opmg1DXMgVv1FlAFiJceSUmkBxjORNcKUMhyzY5ajloo90RRGHI9kAK9jskYMpicSTNZhkKVOPvVChahAGKsF/BddI4ZonI+eql8NTUeE1klHRGFNgGeAqeXkGRPR9uvURG5qUl4ClU4xY6Q6qM/nCRDX/UV6KzlWqpZxObQuDIQpZZR6LuQYVLUDe6rCSPAyJVH1SJKjBCmXBf4rSqHJqMGYV38+FyCMEMdMjVzVypSo1oP9oX41NjUp/4R04tQ1orImwDMAZyJHx5gQrkM0hYsBrDZLD14CxKpMrwPyUViwuInwnwJHZN1fRBfPs5Uc3GKtXA6tC/+OMKWMo+N/THOUY6yBoc4opjBiPJJOeR2iEZI7R60El+UofaN+Jwk1YO6IrQbggECDA5JadgOEXhhrE51fjSzWlX9CeishBB66un2assTsR82tkttUMGtaS53uKCN8cH3v68GibPLaXD+Xu3CtCtOSrwPRpn51Mz8RiLZmbwbiq27bQ7+tQf/Jem9AbA3a6r0BsTVoq/cGxAbEVu8NiD+94tcb85Wa+hdd/386EK96ooWnUubhEDJFfjdsaUvur980/R0PCLsmg3Q1bVro1uj2ocbTZ1JQ4J81L5bSp7Vx9XD1SZXhcczLsXN8Z00OPJt15VU9gWzHj+Mn9lNJ0etI2/Nt1kcuShuF2RCr13Rs+jfR3LRC9qfrQMSW+5HjpOHBKXTUUgFEewQqG2p0gDxmhJWWEuCBCJNqVhn4GRC5Mjw4BljPC039qgJBoRbEUEQ81EceMQ4OQKTQvEtL5B3mqXUI4ZBd25Q7gIi3gKCrQExPnQJB14CgzwHCvTIZpxIt14GgZSA0INXri/Nw30DQyyKLGZXo9ISxEoldR5iTwMP8KCoWxrI4qnJHZUrUgE7Dnxj/XoaidlF9UiiUoNJBBdLhxQJeiOlagDXCEw18SsDA4LqO+8QMR8sm084wBxiuzWEwlQ28NovwCwI3xg0RR4yhqmcUrsdQ4o8JYS3t5CxeLHRw9asdHZTtIpomZ+NstCOCa3DQjj9E/nSoQCQFgv0/4GwSZM4/lVfbZYgU/CIwTYXRQDrAGv4EnxbFDgCsC2htgDoWSiyaU+dYp6ZUyjBNCx4R8ClRl5ZcRwTKltm2h+sJ3uE/DvgcK59EX8MPxXZDwrDJvut1qR4rqXJUEIfLXOrY+QnR997VzzQ59AxhiohKgXI2nGaHrA/jMY4/iuOKfFK9flkjbEQEoSAyJyElANFRAWIimMB7wLgAEHP+BLMWTuUc1dq4eCA62XdyQHRExe4DGhYCBMg+cGlxI8ImHdPOAD/j5eWgxJ1Y+SRlYoDvicKR6ndV6xCPFTAxhDmSOmpHxClazN7OOhSqRFJ1jaDeMVyXenycVcTnpUhJ9Ba5J7j7JLdYMxA2lBgIGtaA6ITRoLPjCiPDgOgLEDrb5mhciQkQplBR7ETIgGAHiKSLvo0IA8K0M8DPGKemnHRcRV9DXKvUApGoqmegPj0+y1UeM2pU14gWCE7JpR1kjUjk5SzA2ajHoeszFVdJRZEnRTIyjE1NuYyIYv6UKtUr6qwWPSsjLTAyegPiMBgTgrtCgKj3CvF8Ni8Wx6AoLiUs9qNkFSXChbpYR2NimHaGyosksDaiSv4EnZo4uG9CQAWIOFXPOMBSQ4HAtVwXeQXCn8WXwBlnmIML90VUNxo7jsn16l2TMhoI97gMhMxXEaoYcDKJKmHVDUHvhJXRIIwi5j9M+RN8jizE44wYTGvjLPf4KZiUVYSNGLxYjoNpWKBU/jT+JDKOhOTLda2OFly2MCyid4CRXAncDDiqYLGmeBwqNySq+khVzzA2BX4V2ALc/o4AtyUMXnPDfkeAYCm5no2zUY8fs3luLf2OGBf3TG9hNKQJnZfmN4BX9927VT+XVNT1FhxO4/SMNLnlbet8tWVvqWa8Xvu7ZBzftq2xOz4LCGUWtc1b7Ig5ELfq/HG1XQXiz9+2h34bEJ9U+w2IL1L7DYgvUvsNiC9S+w2IL1L7DYgvUvsNiC9S+w2IL1L7vwSEzy+zLfVaHd5Xvzec/V/yRPmtQECJWGJIFwkCjv+GPD4t1KeE8zfuwX6ozyST7Y1Ekwduw1Illt1IUpM+FfeKdGfT8FCvsCriWh26Uvlu0iIFYjf+J9teu+vlJ/991DSP8unxJ1L+eJadu1l1divV3E2A2I3FWAM0nJFKdFXe3U72nHwyOkLjd2JCBvoy74RcEOPcAmPNjSQ26aMCMSUrNOoYC0CUYH5cqoNFX6b+LMEDsUdnMR4vL7uxmx7k2IOmeZBPP/YAYo+O3dWj+ndXe/2hAeJB9jsgnjRlfbc5NW9Aa8wr08pkVf8m+zQHYqXLltxIArUMi2Ug1p8p9y0QaSl9IedcA+IJ1+hu7MAbQOxpb0AsX/k7Wtt/DYi0BMRBpy8wMzrhMageByDKYETI3rPnRHAa+I1cqtaGqc4vuZFkVbRBz2V4ohyt5AV1jJZLcoieVYF6aQjIosbCJ8ELxpXVYXwOeMi8vIzd/PT4vN+Pk87Ty8uPn9z1j8+Pz9z9+LcA8URP+x3ve/znkb6Nf3ni+sbfadzz/O2ZJ65v8m2c6Pa8h1M+jvtfXiTd+P1BzhhTQg2euRvMNDgKo6AC0es1HNVjAioCpIwKcBt6YUQgMF45ERz/hd/IqYQ/4Uu04kYiRxCw5W9smlBYI4vqGBMuybllVVDhWWgQOPWxBIY9q8NkJCKA4JGw38mlvrcRMX6XKWXcuQMQj3uG7IF+PPO+/fcH+j6ew2m/S9r99/3u+44v/x1/2zMQ+91OUo7/8ljb1TNJUsp1jxFx0MENTkQdEegsvpoARHcxPhSMPBSI6BxFCn+DY9dgdoCPsepGknUOzzYiHFlhUR1jwiURRkTN2+J6lifoQV5zoLA4kuWIEbEzIMqIKECMizSAeGCQxpTf6MfPx5/S9QrE+K/8/fFzDyDIgPjxk3N+kEWIz65nPv5UIJIKKaS8vEaYI0gouhreomwKRKIpEIkMCImPL7qRBPJABPJALKtjzLgk0bEmbgNRWR3JcsQasVcg6hpRgdjvKhB7/o7U6M4nD8SeKhB8ZPe0pxYInElPPIZ2NuxD8QjR+6Xmrsm0MgoQfXR3Tc3U5DkRFYhYRgStuZFYCvBZIunUFNfVMSZckoZVkRsgMDVhapuxOsimpqB3TU8AYrebAyFTiQHxxOv6nvdhwtlTTctTk2SBEUECxPhVp6adACFn0tO35z3PYvi1gN8RYLaJrE7m+/EO5BJSZsY4q2eZ25mjUX9HHMwn5Ow5EZ1QWthvJKsLSyxMjSU3kqQp9PomLNZhWFfHaLkkLasCQGCxNq8X5l+obohjdcTqJpPxO+KJZBF9tt8RWKx54eX9P+RTAWInS/U/vFY8jvvx7w/eN356eGYgxm/PyGVHnHJc8J8ZCJzJ8xaXRr92u1/d4/Yv1zUuyX2siXlNwjt/We/XfsQtHPFn7+k3bPere9wG4n1KIUtnvw+Ix9Wren7ktwNx/7Y99NuA2IDYgNiA2IDYgNiA2IDYgNiA2IDYgPjdrXtFlf4GAspfIXzy3wPCPSWTR7T7El4dH96OzxefHp5efnJQ5GkMcPwYQ4f8GU8X+animOaJ9/CTSN4rqcaz+Bs4D2PMqTxCeWyK3u3lMfIYFnl44sfxEoTXmuj7PvIsOOR/5TXeLM9qxye/zqKZH+vxd/OGljTyLjO7oryvI5nDkqD6fDG+iVnVd6V03sYnwlnf+MvyUnDm58FTNRA+tTJJZryWXB5RzoGQp/F7PEV/4gDHHs/aJXq40//kDARENNW474H2xmdwQGBPLeAR4HCscM/BFwvW7+SNUYuQRH39NgvnI1VxDOsKDd6KeALCWBJXmJmK6KdwY2/tuJg0io/3WVEJSR6aDJK8cIv3c4U4kRDTmBiEkHgW2J44K3EFiL11tQOC/2+A4JgS77JUI1QWEKHbQHwb/36TkhSIJxx/8EAgpkylk2kGRFIgqASztPOWgbi1twJhivoKRFwHIkUTsqdEJbi0AMS1Em3XN0w7/win4Jl5CWtAUAHim4T41oH4KYwGmZaE94DAivAafgIY5krIU/onjhJX1kQx0lgEAh4qB3VsuQ6E8UMOg/E3LkN1gTmo1khXGSriI7MMRHAy7BBWRxeK9AWAkNqMMcrcqoGgHpFM5cM4JYfCPlEnmIE5BNyV30v373484xPP+f8bVwMGgj/TntcESfcPR5/4G3f9D+7GJz5iQPxvDALqANM8JUwooUNZH5iU8mxA7BxrAiNC/FlEFeQ4nMAqkbe/4aHSi+4H3s02IIrWBUxFBlP1GGOA0fgbp1hdYKK8Yl5UQlRP5BT1jWjo8OdAxgmBrgfzV2A1gCHBI+EgzBfKUOZnILwaCPRJxFUlFuZKMvEAyUTNFDR6JN07ztw7DXuPS6lc6//DiPgp8dgnnlIk5fe9EBO/cTz2JwLj36gBYmcLNef04ycC55X59kDKkpMRUVkTOiIk5IlOPmFEXDDEQasxLp+wTy6iEcD8jWZEpMIdMdrAKZprTCeKI92lUnM69ZGRV/+5rNiOiKgjDnobACKwqEPuY1VBGJPlVg3E6gHvFUdl0NqZmUdH0ukGBD3rRFSmJgVCJnzut32ZumSiesJizUDwkTkQnCPvNSDc1CRpsEZU1oSuEVSnplNhlcyBAPsEQKjWhQMi0RQIWWfU38TICJrbAB8Zm5paIMzEIzj5BtTGVEoMiDBRA7F6QJ2jAtFr7QwIGmxE6NT0BCD2BsQ//8i373td1ve2mGMu4fuk7wrE3u6aHnRqenxqpqZdOyL2OzAk+K6psiYKEMmAOJ/lW98A0atBTlLuSJY0zV1TUfUo/I0yNbG/CZ/TO0ZIhI9MWSOmd02x6GzI1ATZjKjsSNM2YYOrVg3EpkiDzDglOL4wNdlizQwGXhFkjfgJWtT4O+If+dUgI+dRfknwbwsZTbIsy+Szw+8IZvE87UA/pLJMM3OBWQy8/dDfIzzV4XdEy5oQ7mH1wEryO+KMgW2KF4g5i9rRmdTTC78jKhC4x4+Fv5FjdYERjtXZVEJ0sT5nBYKZIEdhQNrviKBeLyidyaf4xQHOSJC7qvF3RBbytFMDqYt1JM8pScqMcYv1x28P7sb49vZn/rJON9qT3nz+b9v+zkccaQPiK7Xnj634BsQGxAbEBsQGxAbEBsQGxAbEBsQGxAbEn1fJP/hSEDWCfG8zw8LZa7l+Xn0/AojwQTVxsfR11oTJ/IuKbwZvQqJl+qYcny2v5SZwLIy/0C0Um9DMbKVKuqEqfaQ2eW6+3Qo+z4LBab39+Vq6MTQl3iemrDz2iXsymt5aw7jaxlTzieusCa9kLerVGsYIVZvY3gjVV2wN5am8Qpxa1Wh63yVLofxITSmTZsbVg2kNoUnucyA4qhGT94J3ph6vriE1R9O8RrkmSOvB+gqEvMWfKhC90Vs0aIgyqnD0cpPvA2KBh5EWm+mvhfQaIMI1IFSXpNeWBpfmChArNaTm6HUg8hX6SgFCvD9yBeIgQFzUsSTqABuHMyFQ04kfiQRixPvEHFIYdw7QwCMFdcoRrIqjuqJ0GdMWeBiSSzZOBMIoqu6RcVQmj2ysDcvN2BmVs2HqHADC3FTg64L/RW1fgTg0QByc30rhX9xRQz0nm65IV7glviayL6yyJlgRA+oc4gCibynr1HTm6p3UscSAEFZFwtRksWX46phDCoAwFxSgnfFie1ajgWhiQog1Wqy4F04Eu5UkUffoo4X0I8G9B8FHy830Q4yzUS0LdC3TWkX4usj/HghuMcf7AATSOe+VO2sI9kYobjTFH8MICLo4FNGGZdYE3w0dNJ4rHAV8E5UBZk+cCp8BnXcgDwT0LbxGB4A4FRcU3G1BmeBI8EAxamVStgXnAjYQQvHdmOIUq2dKp3oexouw3NTZpXA2NNRapqbuUsP/p8gMDmZoAIixfhe02IBI2uHGu7q3hlKzUjtxu1BKg68Jlt+rrAlRtzDmQtRvZbE2IEjNLphVwRMLV58D+qxv4TU6DAguAR4pFQgZEZTctFq4DwP/30cTOkl0Uo4djkblYCSKLjfNvXI20J4yIVitAAT+T30ZEdpiNyKo8voqELdqKOVTKlyO8UxVYUBNdLonEZlZZ03I/7aYp+IeBSD64KYmsEjNLkaBcMZJ5pDCacYSeISZpQyXGGEpZcOc7BrscV5WZxbhPPTaaePeqOZSSIHJyHKz67NwNqK5GynQqhzC+8Zzev6/O+OuSd1R3F3TQQDtvSHNXTVE/lY7VSjxNcngcYjs0jprAgNR1TqyOZ4wS1q4DDlHLNbMYoAeBu66wWjr9LPAoQ4pookh7iOyRMk0xN9lnldtDVusI8GZZFxI1ZmF1AgNC2uGZwqzMoZcWBuWm+mHFM7GwByL+pvAlEN433gO1+pMEb8jxFslQgFE9UOo0ia9QsmtGoK9YbXTPH1NEpb3ZfXDO7ew+O2a2ob/3bqebkWz7646taT89tvt38IYs28t/b4a3leTD9iuMXR8M1/L5En0K+r+a4D47dv20G9r5l9Tww2IDYithhsQGxBbDTcgNiC2Gm7N/FNq+LWr9+dcxe+qKcy8wytSv+VZynuevvxtQExcRpL8MXv7ua/KwpacdPskt/kmMutIUc/Trfii+LrM/FpsZ2le0tjvYkqBu+ZpLBQyPsqiBgf5nJZbkyY1vMYneXn595KaHnAXYXWFDuTtNOQEjbxZU29IDGd3XZfc6jUfm2/mKz85TzsnTYgFtGATQmW8asQm47sHos3ZKWEUh2wX5plrcLiujFeeCse7gfBUhLAyf5QY6scAsdANsy78HCCGftoBCzUoQPQFiAUNjk8GovkXfAsJTxBejgUvgfcGnTIYCGNJdIMpTehrsaJMcRlqasm15Ia0F3ihyPkSGFJflkDteYeh8UWhS+FgmF9Ly7rAxIlId1DC2vlc68i1uAxzhxcOAmmJV4E4ON0OYzhZ7gh8Od8XvKh7xj5jkRxKagBxGJyPC0FXRCZH8C04YBd0QsEb3HiTvwJhLAmO5HLsMdpb3smi175Da25QnTjV8KBQ1eDLAp6DnWcKGtUXhfMl5WSYX0vLugAQGBFBeYcn81nR/E8LDi9jbgklwollWYMDwdQ+mnaBEalq2JPI+b4I3YB1QpStYYyP2EdzbAnawqhWD70YQ6SoQCAkykoTAKK7jNevOKhUIIwlkUSPQpgRURgIcvYp1tSYXCw3nAcgeHniM/gIdC04PNue531RJN8LOBltsN9YF5MRcQEQrKaRIvLvqOgJOIeXautmEidLGhxkDhWpowoEWBfC4dIamxAE63nYJ2ORoB7m2BLUcQauFRIdT7psK99CRaiCFk5DyjT4NcJYEqmYdECIoZOzx8YOfq6vuUF1AkCQTDsI5IfKHGnO874onpOxzLpo1wiUKBS5avExzIFI5IBopqY1IJh7AbmJqq4xtmZogUhURokxFwflSapjS6CiKqJASIR+MCB0akLDIjgMPOVIF7HBRdW2UIZDH+Gg1aPKQqzR1DY1ITeoTggQ6OSeVTDgwIKpYwEI8UXpMM304GQssy6CuD4UIHoFIhoXy09N3uEFXWolBlrS4ODylbuhah6d1ElYF9RMTdoyKbNHm1Ml1/SHwUS1QvF5KSOCdGoC34LgK3LmWTMMsAcb9yYMLwABroGpYYB4jAUKREdLjcXacoPqBDuxBDUtC2QOLAfQJd0NkvdF6aRTuSRhPyyyLqJeJjhZaiV16ZSJcZSlfe7wwspKtURInEw1ODpM0yqVxTxHqPrxN+Wb6GKtvi9nUEnRZmORSD3IHFtC8XlBH5j/TOmAyneYixMm94vQH03x3wvQbPY25/ofMMu5r26tit/yrWOEDwq9c7uXN5LuqNPSmUlLWarp/R4ztPbTHDjeYmPcV/W3bSj71z3iuJd5kla+L9X0VWyW7aHfF6npBsQGxB9fuw2IDYgNiA2IDYgNiA2IDYgNiL+wpn9CZ/7yOt6jrPHRGhp/BhBv5ajct030IvjJlgmIN1HY9uFUlqSTXDoqL77Le3n+HM+BSLO/a0Cg0UtqGKmtf1o6O600Oa12Bp6TBomxdJPaFi5LltheNC3jtv+S1tjeJ2w38QO/cN/IM9ixr+tjUQvzx5K2Sp2vA1EKVXZGw9rAq62pqDrg2FR4YQ7E3OvE3jCddmGcApFpJiExpRrEZn+cwY38JZKQkm9NBcKfEVv3llZvw16Cr+m1mhLvSBqeDpnfUE+z+ikQfD1ot90BhFegmADR9IS3lrkHCHozEPRqIKgFQh933wAi3QHErHS8AG2RPA6ollTKPZB/mXfA2iyiIpHVkn6RnQA2hrEzPGtDWRgajuwKlyLAo2EwXQ7Tt4BqxsvLugMKWtFqbBifw1gkwatuKDeiltMVzY6qC1IZKXyGhXlaIHD8oKwW47IoENnCrV3hcNQyKxDgmxiHJWYDgsqIOEAoHq92B3vtWoDoNXyJwPoSO0GKGtNpLNqxNoB0CTvqC97K24AihjIZwMiAasbLS19VKiYOKLgoW40N43OQ+psEp7ph3IhYGRORzDxAz6USyIxqEqVB2gYIxJv7CFYLA1E1NEKJe2sIufdtcyNC+CbGYeHzjtlmiyjiTMrxSMJ5CIWlIUYTpYg1dgL4UKkCUVgbnZuaoowCed1ceBuUlcGgmhbySVKw9MKaAwru41qNDfMoEUYJrt6iumHciFgYE5JOz8H+TjkWRzuDwLMACMdLBUL0M3h5lakplmhb9ECUvqptKyNC+CbGYYGkhCcvoOdTrLQq8/jIlKjIMKyyE5ifMAFC50w3NSVRqQARJRTJnVhC5vJJVDMYiDUHFFrQ2DAVDKhbSK2L6oZxI2JlTDhfEyqaNqS1s+mChuURoa3JLRCJvFiFB4JTHAoQ4JsYh2UZiMT8kGZqwsKe0G1+apqyE5qpqeGAFLEtFgQqJBbwNqLT5ShCI9LtAsSKAwqZRojT2DA+B5QvSGZsU90olJXoOqcH18LOldlPa6dn1KkpeiAOQ6+sFgVCp6aMWSAXIHrfNpFVQt2Fb2IclgJEqsu+jLmzLsXHAdwEzhEOg5PFesJOAJvjMICd4VkbTrEjZ02PY8bxDspdEArkANUM+JAsO6CoJkijsWF8DhXgYgZHUd0wboSVo+mERWLnsoYGae3U3AzKIRdVHrkU6ikdlNUCIJQ7PqjOBpF6o+hNQ+FlqKAXtEfOxmEJYIgw19yNCJMQW9wCrd2uzrf32lJ+/C/rV3Ejrud0tY5rPULxU+ub6LO2jwbi4zx70qfV8XPq+8WA+K/WcQNiA2Kr4wbE37htQGyN/KvquAGxAbHVcQNi65T/iLzK72nk63ghr6vjvT4uXwKIrrxg/fJizihES5oaSf+FjvGUr5CaM6v6BR4CX3mmkpuzJ7yQafltM9Ps76Src2r9KjISppu9MrEXWVX5mCt1zIBI10pxVwh4HBxFhVC5Fd1qaoRZ2H7KV2iPVvULvE+8/JSxNeBY4oVYmqCd48kG8TYQ5eVgV1peIxf4PYmmBIG1s8KkzBkQ2Z8TmrPTZLgqr4CBKI2ZaWq4iiQHxBzdGNouzjA9uA3EEi+kTfN6IPp3ABHuBIJeAQQtASHv0DPL4lxHRKfvFBs7A0yFMVRDqjsxcNZTvgIfATNDgikZPgnKpYDGQKxaFpwKShWYCq/xQoofCeGl4JcXnLfGCrGzcgTn5BA7fZm209JCOa9z+5nTcSntOA7GZzHGyDo3xFxkbD/X0KuWYFqeclRM90SBQExZXinXNSJISL2yMxDGzBJ77dUAAW8XGxDczF5fAOcgKgKnUL+QwCiumkQ1EsyZRjiR6Nha5YUkmJdwSRLl5Iul1zf+l1ghdlY295MzyAY11Bsc44ID2tjP/I+TtoOgsZFVBEAYI2vckONgLjIa4xZLEK9agotwylEx3ZMChBCdog0qc0ap7AxzK7G4M/uR4PXv5MLkJV6dTFgB7/rzsn4gZYhYXFi4Neic27yQTvxW+HsT46aWOVFZIcYmMauR1LeOJsbDgDnDGBy+qMCFAIF2qN5IRjuMybLGDTEXGehusEhAq1oSzMOl4aiY7gmA6FX2oABhzihutFALBHRmmJfg+Qr9ChByNB8vwat5KBCJDIhrvBDmaEQN1y8BgfIrK4SUTVKAmDia6BqB88bLwzgkfkSYe5GtFi1PY8oNMRcZcFdoYCC8agnu3KYclaJ7Ilfi2aYmk0AB08emJvlfCjufq6JGGQGVryDzcW9A9FgjCnSJqI4c9b4zasuUsrbACxG/lR40lhiN9bHGCrGzzucChHM0USCMcSHzBfbzHH6KWCPgloJL0hgj69yQ6iJTpyavWsJKIXOOiume6NJ9NB5HViBkDxZrMDDAVGCLMtOdIHVMcXwFdtrBIiZqHdCyOA5RmR3Q6jNdjE44F+BxBF2s13khzMGAfgXS81LI/66xQuwskH1p4miCiaAyLqS28GMZy8g6TpjpAZJcYYKc17kh5iKDMnSxdqolrBQy56iY7km5x/a3YLjxmlBs17ep5NvKt9dt67wQ1Pazfv2n8je9sx33ucc02/TXLrJ4C6/gbVV+3fa5+htLQLxte497zEIWX3XbHvptQGxAbEBsQGxAbEBsQGxAbED8MY3cgNiA+FtqeK16X63q99Tnj+U6bUC8YUuLe82Ferb/rrzaipdzIpxRZmoHC3WxB+P6bejc8XCj7iXNqt9LZZ0EZ+iz/m9r+oPnt+uVqIwXPRSb51pr4e/lxiypAUeiCS/hPiBKYD6G1wGRrLxo7782dbgNxJrfS4zvASLGpuTc9pBnvFwFgn4DEPQ2IHItWd91fiUQRMt+L5HeBQTdBQQtAoGwzkUUM8zfo2puXJwLCtgIFOFJYoPNcgjuM3gQcHXwGhocLrlUt5OizwHJdDTqui8JOuEgEwsfEVUDZXboscKLWPZ+0TJW/F4imfvJa4AwvY5YcpQQkip8VL5JBcJSdYV5ohoWJ3EfMX+PPpoDiQXSEeCDLkavkWChpSjDgkq41HIrl4XT0OAA4kk5HH1MRZ9jDCIWIK77kqATwAjh+hl7oo82InqwK/Ka94vnpsz9XsZ4ooZC0cnydviw/i/qrPwRqY/WNJmUUIqVb1KB0FQ9Wsa9oxoWJ529OAOQTswXgpe0o/imKBshc0fbQtddSHMIdCyfT7Eup15Dg7kNp1jjuqbPYc4o5sCy7ktSR0RQ4oDRWGJZyPn8I615vyDNmt8LeCVId/+IMD8JxMq5VADBLI+kPJUp40VSRbTsKH03wKMEphpwUugdEFCoiMKNQPibgaDBVgzSHJTtV3KzqanR0BgqEDYiqqqFNeqqL0lZIzwQYHaUEUFgCi17vxgQy34vyit55RrRN0BIB/P6kKFzkuT4lPGiohPSMu4dgoYFMxeqvwc0N4pTitrRqIeKI6kYmyNGMD18bjYivIaGTU3QuAAQYEOEenVd9SXxd01uRJBJHOlZPabYJe8Xz02Z+70Yr+S1a0QDRK83NBk6J9zWOGO8aDvh/MKTJjQsmLlQ/T2SMi3UKUVJl1HZsZ0yEGRqOpPmYEwOy63cvzgNDV6sYVuGMkyfg5kO8Bu55UtSf0ccB3SceLMoEwQ3smBPrHm/oIw1vxfwSl67WCc9S6ems7o9Jr3xGNsaJowXTXU25xcX1U51qlkXJbxbMWIth3r7mma3ldNfqenOPGluHrh4PMX769nW9VaadKOnUnsXe0/XAZm16t3PRng9EPQOIGa+povHl2qfPgmItqxEX27bnjVtQHxoXb9qnTcgNiB+T12/ap03IDYgfk9dv2qdNyA2IH5PXb9qnTcg/u7tqzTzP9LdGxBlS1S5EPx/5464zT2yqmyHmiq1mc5izxYxCOUpahT3FnsuAyVon6KNCDc11HOgXIyXhslqHmu5x1wLNwMEV/eFh3CoRWv/zG+IftzWLfRTZ80LTTNXPEl8pzYdfB8QxpzoSlyAdQ6oxDbkW5NiHYjo+41fWC+EmtAAQQ4IfSXZ1z3O0bBaLAGx9Fbh+gPVqWdLUKrDJBfPKHkDEPR2IA4FCInhJQPCQpQ1xR1ApCqvXrrqFhB0DQirxRIQt7p7+ciSsMs0me47DGAccFxL2RWNykU3VGZF5SvgTChWeBUMPWfOxsiNDo5dfWIJdZEJJkxSaNglFfcUZWW45vPZHJpUtgc4HSjXgIAyCJXwUKTik3I23gnE3kVTQ2tRFTdyZL8XKIHoG9ipHq3OLJ3oeFhNRPsjQaUDnBNhijj2SHFi0X3EMVdlHGRwJ0J5y776gyRlVhhfASwOvJ/fqmDYK/GmXlHYGLWbU10jxhk7aWTbrRFIYUAU9xRlc7irbRAPgGiqBxI4lSCkB6KXulMau6bnusvr8xGpwDtJCTyVulKZvgfrebDfC5RA+qKnkYo6wXGo6iKnwjtBgLhXtkuQPpapqbBHzInF9pGxGpJoQoBpAH8R053oZMAaXaYKixgLolXBMMbCjI1RgLArX+fIbGyROoEhhQFR3FOU/eDXCL6CjepwvITqUOKA0Ch0tvry/1ABMd4J41BM38h8TpD+JCoCqgRSZBy60nqjKbCWyCkeG6+WshpSUiAqe0TPLYwSMlZDIrCBlGkwVN0Jdh5BU/sZEKZGUVUwormqTNgYwS1aHgiofBgvxKfwQBSvk9gCoeF+JTIGcyhZACLW+hpbo3BQutzlRC0QSdMbEBb41w4eqLjQmLOLjAiqXi0YES0QlT2SWs0RJn706qii3Ic6NcXUTE0tX0GBGAeYV8Eo2hszNkYFwt8QoMGVF+JTNEAUzkNf2Bva6TyzaD3r1ITjBYheF/pehX2UPWK8k6TiLMEDoS0/n2VqImOqxAJtchNRqlMTVRupw1DzC8LUrewRc2LRfZ3sPhZNCHFDMdqh8wfB0ub5CuhG5m14FYwuQ3tjzsawcQBFDmsy2OWVF+JTGK+juKcI50EXwyz/iT8Je8CAt3HUlrCuCP/HvyPAl4AGoS3WytYoHJROb0G6XDvOvGHY1OagSiAAwngX5gJT1UVyUQJBvna3dRQeeBg8e8ScWHTf+o3Z0nY7dXjDOWvbtV+0Yaai97rjkzrGFJdrf6seb+uvt/fJOzL4HCA+duuGawyVLwjEr922h34bEF+yHhsQGxBbPbYO+IL12IDYgNjqsXXAF6zHBsTXB+LvEyf5Q6/+DYhP2hzDYG7NsVztuHh2u8VpKd2tVOuvWS0pE8T1ulvHdjfUQmqLFl7xSjQrm3MLC0lW6+0+257j8O8lTzkGs8C2Po2Mk1rUJ/bRARGmZ690seURVlOFFSDiVSBCWwdXHuzAfZmBroX8DYimtXkOxNTvJbn0cQGisAAExyjylMCwDATNgGgTtCNi+tblW4CgNwGxXHd7RTLOyrwGRD/ndtwNxFLuyfVPaqCREFzSMySIg9DQUNkcCJp4BxVLURQ7RL3i5YUDNcZvCMZnyFUT41LYEsgDLJFApq0BhQwwLSwnf0Y3TBglS1odhQnBNYbGhQ75zGLpRy1TVUXIuBfGOUE5cF8Zg0ARcj5yDD4zxVHFaiCR5qFKwR9NLUQob2BrWHSuMkBC4/qiUbuMgJYyiHp1BpFgYLTgYnVQMT2JWBQ7EBIdB3Jf+Q1B3VLMg8SCh2ZyIElVccC0PRBgB9PCclIWhrMacIySmVYHczOMCZFxfs/cC+FPJMSbScoUvkUyEwYwP6LqIJC6r4zpz8nkO/SK1TpJvhoELi+uSz1TVFUP4Z9ExKaNwVEZIIUFk1Hvo14eUp9OBwm4DQFsiQSGgxMLudQUqQAhviGO3xBKcF8dR5TXgMA+orRRI8vQ5qhA4ChyUn6F+pV05BklS1odlQlxilaHUx0RqhUCShu8MSyuXOoZTUGELy0Ovkad603lADwWU9QA00XdXi7chBKXz531kqkzlHaFhu8SipUUXF4GAwJMiwTBB2qBSD6FKE0E1dbw/AYbEWXWVF7DEhBJ2R5d9kAUPkmsDBH2J/G+KXOtjqqtMQVC1ojByoSqyBQIrmcfTdhCDDioAtGoahh3A5IXRMZXcUAEJSG0QBRnGsd3qUCIy4t1YIwlg16cQfrGQSU2KXDXFHVqKvwGFI81ItJ8alJfFJuaCkDGtLCc9IzeWFQNo2RJq6Noa5zPQfke57PdNZlWiE4C2ayrnDMKK4P0Z1HKUK8WzGe2vJpTSpma4EJDxlcBO844LH2MHgjPAJm4vlCpFRgXR3UnibDjOgdlODgHFZcCDAaoV/BiXfkNauZVPUiU12BLL+cRjWuhTivqy6KqGchJz1DGxYRRsqTVUbU10lH9UdSsinixhlYIFuggtQT3QjkZqPNZaGMwf1Ntj6hyWswlbBdriqY9It/PQTksoi1yDqoPYot17aHW9aVMTUPLrr97qz95ln6HpleFxu/X95iXc8923y/l1Nzu3q7f22v9oVtlPCwD8ba8vgIQ8/TL9Xt7rT9p2541bUBsQGxAbEBsQGxAbEBsQGxAbEBsQGxAbIyNv7iOr6xVuHF0A+LTa5XwT05+55SdkWt2a3yIlf1zPsiiTsdavcpZyew20mJqVjFomjwPspdX/+pbgPf0TteUmmZ1uK7useojswpEaxAzIwW8vHhRg7g4ataB8HyQZXmIOXaJmrB87rTktJBWUg9TIMIyENncuuN679X3XhsGRpwQAGRfnLa1LRm1ihOuS5y3Pk3+vR+IcDcQdAOIcAcQhxtAhFsjIr0GCLoDCLpraBsQtAIEBspgQRTPNyhsCdGTAGPh5cXcUgKZ2DjCGsaF0DeI06XwI0wHw/NBlnU6OKrnHUj4HFP6UJUMfW/aK4Rw/sfB+BhRXsSt3A5mVYhbCanLSbacSAQpOj3XVDNQK3N/Mb6K57bU0n1fwYflYmXiTOdPw8EpcDs4D8TqrGWd+LlILnBb4M0rTpjkPvMrcCqPCHOaoGgOJKTSOykWlYl0qvwIYWd4Pgit6HSAGFYDqnyOKX2YKQGAaBRCkrFLhNUxAoGa9MLtAAsDTIterRXU3iDJq/CQDimqGdVEAIok4Kv0FgfPnk3i+wo+LCdpme21s7lkZn9pmDkhci16INprJ+RiGhwy3BwjQ7gUGl8FY4GBOKr3SieKFjoiLsqFUJUJxKk1GpzMh8JoCN1lTaeDj3WqYgFZCFP6AFejAqF8jhL9VRUNAEE1bzRd2BYXIydoTllGhPA7ook6SNmcp/wtfBUjNBwnbBKvzoFLdMrYEKejC0f2wUhBbXREqHsLs0dkj2lwGBDOakKb2qsLiIwIMu+VWIQcjAuRpiNiEYiS90yng5SxkagCkZS9IR2dg60R4HMUIPT42EGoo2nVSDBf7gV1dCsQzHM09oQyS2REiOYIjwgyIMRhpiqVDJ5N4tU5PBCVsYGzhWIR0UquTRD7ExpMD8SAEA0OSB94vkGdmg5DmZpkAMr3PjauKuaLgjUCjioFiJYP0i/rdBQ3FZBuIrxYjGLWAOEVQpKxSwTGhOmT3VdkmkDTC/cjFyBSYU8k8YPCGiEaM+eUjLmELi9TU/JskladowBRmChFVqUH1wX0syi3CUJ9UEYL+89ILqpwoYt15RvYYs16EliYX16CEBPl+zkM3lUFyhaqMqGOKrF4mjR8kPOyTocs1kUTQzwOE7QujPPAeksysXiFkLOxS+Q2QIDoVAODWSFHs2WTemQqOUHMR/gdXJqewW0yn5PB+CoH1SbBt1D2enUOLjVHlIm9xZ/mLFPTuRMlDtzmgOoKRgv31rEwPmY/kf69+Ns3fxN2K2if7kgzz/vW7/p7t/vqeLUmrRfKu9gadvZ9zJC7q/i5QHzM9tFAvI+tYWffxwx5dSO/8vZXPPT7Mxu5AbEBsQGxAbEBsQGxAfHFG7kBsQGxAfEfB+K/U82lGv0HFGr+FCA+5mnY3VvVrEiEgBBvrcMIjvpzqD6jmT3QWji3sTXmQGI9tqhvsfz61FoTrlpBaynDPUAch2ZffkN3xjefE1xwu3PvliUiJ67eBvCr/kVYKDrQEhCR7H1sfSXdg9qE66mRdIgFhdTuaRri6+cpFqVtC7WsF8H4MF2CCf4p6fK4vUY+iIuH2jPiPUAEdjaZAUFXgaC7gOinQLiTDjdGRHRHA621dQ0IutYBrtN/AxBrOhkkEbGDhn9CdQ+hlstg7A0N6pA5k0Ddop7bOJGUsAv/y0Acy8uz6jdCleNBmidUQ/SF26L50ZGqZjAboqiI+PodS9nMo7jIfse7UGUMbpcpaLy8wOAG8UmwSBgIVe04W84pImRUS5BLaTDtEusV81nBdFfP6JStIkfNqmCqk0HCrOuLeHmP+CVZcLAvr6GDvVEUKnqcDS8SO1fj0/Ji+AjBGSF/BEF5jVBXE1aUp6qB0UcqL7kjD4t8UzLND0xNUZUDLDQbyXuemAsKT00gNjjeRUyaF8Kb0OPAiGCaTQ8uCTMxRKckasi0x2vxxcQBJfA5VbuEmSSkNcOxo7N9sLplsFQEiAWdDIwIijZns1oFgPBcBs/eYNZHqC4jTobBeA/cAvnUg53BIVpRVIpwNTFeiGlgcJlV/QJHoeBBqerbg6MDfompiHjlDhw3d4uT1tuizMI1AghFuMFGxEGlKTgPAUI4H6oeEqHYwUoetQRbAbsivNJdtGaFZmdnWN3Uc0KYBks6GWYlNlhnGhXFcxkqewOsj1BdRhogEumIgJ2GsTMicxkABHMeElUgjAFR1C9MeELKnwOR0CBVEfHKHVMgUG8DgvVCWIujBULXiKxEAjIghC6TjYnCQCQoaTggwAfpi6EHapY833GIrm4KRKJlnQy9LmOKZs0iY9umpuiULPrZ1ETmRVLOjZhFjVJTBYBiNLWMVGzRTAOjFebRqSnOpiathwKBqSm2nicKRLSpyRNg+IipJ6ENBYhkjA4FAnohtQ2mwRE9EOCDHAZYOXHdGyCi92ExtooJZy3oZDBEWFigRSNCIAPpYmlchsrewLIEPxMdlANoOsWdBByHwZxIsFiHwdQyTCCramBAVcOkqqoCCJEusBl7SFkUatx2rvUzzxO4oDCPImu9C+8CTi1SIhQ0usJ7F3JNBItEF+tzVPUQU+zoBl+C3etxjgE1P6NmxbqnnGF1S+Cm32YUvOcX5T3yg7XZfu/1eoGt+3G/dVNT5+uPOG71WLra2ulW+DK/TlXiFhBtTa7Xy7yAPqdu14G41WOvA+ILbdtDvw2IDYgNiA2IDYgNiA2IDYgNiA2IDYgNiK07b9SwreO1Gr+3NR/UG2Hl87XUGxDvPD8t7czuQF49w+gCGU4Sa04kc6eT1PI+pp/L1jUl8l8LT/rk/NT03wueZmYO+8SJU4m+3S1A7Mb/ZNtrZ7385L+PmvRRPj3+RMofz7Jz9w4gdmNBWt74ZV/y2j3tfowlP+0envBpMcg+ByC6FrVAmMWLByI2TiTeYMPYGWEGRLgyMisQ9rZn81Q3ltBUlNcoY3UqmQGhXcF4vLyMHUMPkuRBkz7Ipx97ALFHt+0eZjXb0UPzbXrUAfFU8WiB4M9Pu93+abenDwDisAgErQBBq0DQa4BotDtcjDAOY3iJUnUqmQHxhCuUO/IGEHvaGxC0AAS9D4ix+3nXCMJuvwcQSZUrVJcCihNUuRyyVe4B8yOqv8hR4tXc4RA3PwhjAY4ixt8I1FWBdWVCWACHqPikkGl/SC2i1aEr5RnNQaJ32VxbWiDO/JpuNqeSWk8ui4EYu/np8Xm/Hyedp5cXnhIe6PH58Zm7H/8WIJ7oab/jfY//PNK38S+Nf8d/n3+O//94Hiex8dt41j/y7ZmPPj7j/x/PLy/8ic99kP0/eO9PpOb/aJyQGCIPRLbAHXQpLOBXuBwN94A/wRWENIxZgRCzDwQsyWwMwAuhYjkAxxZ9hZzfvy8+Keb7YK+wWx04JAo2B+sjVCBCObcbwP3g8k/xmAOZU0n1QeGyGAgeCdz08cLc24gYv8vkM+7cAYjHPUP2QD+eed/++wN933Fvfd+NM8nuf+P/yOc7ZzNe5Ts+l/jYdz62k2lvV8/lFN9lLOy/c2lc3v92DyM8/xMgfjyNF8ZRORumSwEuBZXItY1ux1aIpndRLMB0RHBOp2huIQjs85jpihcJc0D4WyCwLczFAUcg6gCGRwVCgpWqGFKBqOdSNB0BiQ7L5Gm+DPBBQVkYETsDooyIAsS4SAOIBwZpTPmNfvx8/MlH9wLEXrq8AiF47h9/7ohz4r97OSZAuHM5BaY5gWG8IdiNQPA09b9xod492YjABG26FKZjllsgUmUrxKi8D9KpydYIzukUkzPAiMaN6MHGYA6IdeYUCFHCEB5GhFpFGRGqo8GTTVkjloHIUBApQKieCKWyRuwViLpGVCD2uwrEnr8jNTrzSUbEDIjHJ4FH14HvDgicS0/Yt5d7IwBBz/sCxHhMgTB3D9OlyLrotkBUtoKqvICfYJzR4kQCcoiKhQgQwuxAYF0noN6mpkKZcVNTlHLHeT1XhxbYN8kacba7JnfuBIjsgShTU9C7picAsdvNgZCJxIB44nV9z/t4r3TfONsXIHZlRIzTjUxDMta+PQsQOwFCzqWncZ+mqFPTE/3zD+/hvwZENCUL1aWwmGyje1e5B0GUhqClUVkX5kQShUAYBnMiAS9kXCp1sQa1UtUtzp2WX47o0h6V2GYOLaA5Yj3ozI2lnjsYEPgd4YGoizXXkG9Kn0iW0Gf7HYHFmn8x8P4f8qkAsZOl+h9eK3g55puex+en8fODLNAyIiQ3PsrHxmlnBOvhmYHAubyISHkChC3WDLLkuOeLQH5HYPPaG2/ZbmlgptcR65UetpBq1bjzVgvCO39Z4+qfb/vm3nW/cP6eful2HYjXckV8+vRhNXwPEBgR8+3x+Y8C4itsf+BDvz+s6A2IL1L0BsQXKXoD4osUvQHxRYregPgiRW9AfJGiNyC+SNEbEEvF/l5A/gPsoz8fiHDj+6399x5/4zY+SXx6+cnP/8Ynjz9/yJNIMBs45FRSNefs9vKwmB6eHjjWRxJql82eaSZxdh7DR3h/TF7q/ffCL7n/K0EaYVvIlmPI+DY+ec3/ymu1cu5g7AwzblBOhz7249T6PDjLi78ZL+FKKG/6aFAf7HMUDyWNcZRBXnDmBC68LZ+yBLwG29c6oKQmmi/pNVxgrZBnwNJmtMZysVzF83qotdP8x+fhY7fu5RNCGXtjLTggsKdQEPZCOiFJvOcQi4Xkd9IVoAcgKIPWJqLyjiqAsA7NkTtO+mP8VN4mJdPnJqXrFJ5HNbDXSArekuW//IQ1ZAZ2EQhC4vpiLaelxtIn6TUfnFpHa/mRprCUmqFNGhWxl53dODL7EwtUkfaKqokgLvVaIL6Nf79JagXiCccfpkCQaM8vAZEMCIlTk5EA7gfC3r3Gy9v8N+ir6nMgqnlMyZ9sfCwBMTeeuQuIhFYM/RoQaQYE3QLip/AWMHkxuwHhk0dlI2BqQoyKo7983LgR8HYwIOQV9CUg8r1ABFXqCFSVMOTajnZ9w5Rj/JttRJj/SeFxZExnlKGhcRhSGRGBTG/DeCsBEXwVZg9FfYSl5o3ZchiK88oZbUGbuOai898A0Q1TIEydpJP2FSCYRzD+98SxqB9CLhkDgt9t8tLwuURedzuMEE5Vgdg5boSNiGM+ZgNk/DxwgWYKcoqYLdUlIh2zAcGfxlk0Q3He1Db0tfrYK3dEru1xt9jSjB2AvwwEZ3eK5BQzSF/JB1i9vDfdRzOTgVZIFG4JFDNw9R4HIy7AAQXmBuoKQ9BR0Hh6DyDQJjhCyBqRrTUYNZB7QeuKe0aqU9O4UGNEfCMJmO++kQKxs4WaU/z4uZeo7K5MVe2IqNyIdkTYvyo7pCPiVNYIYX3AbOMibiMkGhjSoAM5IGJQXxRleDB7KfdGPMBffVX9FE3Zo3q3kExFQew+QOmpkwZ8UOqUEZR/YoYeRb0jxRJrFNcNUoaIAiFrBDQQaDIiLkbw0dYpq8UDsStTE3MW+PtTCwRzDh4EJAAxnZqwRlRuRLtGeCBSXgICzA1HlRFmiXRCtjWiGGsUIJRN0gBhAdsmT6ryCxLbRndHF7zNSoeYACF16FPRqmqBEAehAecEaoHIMyBSDMV9hlt3MI0RB8T3AsRegdgzlwFTE5NI3NS0a0cEuCG4a6rciPauKZC7a0oGxPlsDRIlCnRaX4BIZqsT7MYKVk1wYoFlDM8+0AVRTk0sQIx59jGnat9UpqZYwPRAwB3FjbYyCrtzsa8RJR9Q38AyNJ0Ozj2Qtakl9RQgegcE1/vcTE0M5eM/xiNg5g2zEPj749PTjhfrR12sfwj3gNkKAIK5DuMqMf6OUKbnruVG8O8AvpPGUoe7E2hyqHFOsqh0NZNCo21CSULUlA5MwiFhnscZ6hwdppmM+/GQcSsaVUNGcj+HovohfBNdrGOxWHNR8QR3lFQUM8BtVH8VM3qTNOoKQ1AWwWIu+hyZitGaus7IRGu/I7j2olgy6HRpsl6yPwyrSgpGyr0v8P2rf1mnu2rit7f+Gl5nn6T35R8/pn6/GYj7avIR2zr7JNEX3LaHfhsQV2qyAfFFarIB8UVqsgHxRWqyAfFFarIB8UVqsgHxRWqyAfFFavIHd+KfXq/XnfOZqTcgvkjqe7YwLcD7hritEXhot6T/tvwGUVG+lNd2/ZFrOcf6Hee1IivKjSjnWQU6l6uUJbLR8r61siTETIGf4OalNoSJWc11IZgwyZtjdmMEzlgnl6NqhIgSP79QnMFCYdaKKYhk+T8UBXTiQG1ijgv/PxEzqZ4jRfLEVWvqfh2a/RpaKYHxMGtYWMo51u8tEAhkghsxkYzINBGBIFIOB9XgvL1XinBom8FbgbC88Wp+YZ1QVRZXyfvkVUMQt8iiECIvx1p9SmhpAQh6OxAaAoJ4elpvWJtzuglEegcQZOLunwBEjShK3NveHU+meBBD6QsAh9BYBWJMkRogxJw+6auzEq4wYXQ1qh/wXV/a5aBI46RyLJoYDMRBI2idRrFUU+NsbiyTnDP8Ukyx46DlmYcDuBGhvFB8GPDNgvGeS3EdiM5eKkag54OBODggDuXo4QYQ7P4xtoipDLxGSDgxJVXR6PFqeV/JL4ipUYxl2knVSaXsT2Bf5HQO5SpJypPA6+qaW5NzUL8UHdix1/JsRIAbgRfkodMBS6mituG4FGUez67zL3WaMt8W7OHUx8H+W/tse3zeWBHQCl4NmK+RzhqL5BRnHMfeo5rgLAHBsUR2Quh5gTvAZCZBbcM6zTC1pXI85RI19IdrX51Uyv50UfZFT2XeHAuX63ksIZkbS5tzVCYEgADjoqM6Ik66Rlgd1dXlUrQFHJcCnXWgOiKoOLGY2gja+dYRcajuEfBUqWtEHzR8nnCp6UVmtThQzKyZc7y0QIi6FF8449SU5VTuqQFAmLyCBOz5DJ4uwHXwq4GMiLqfWRftYl2BSARJh+KPUnJGXiYKoeH7wY8IAJHUcwWmO8mNiMql0OkDLIkCRAFlKL4tb5+aNG9jh7SLNdXF2oCIpRYZQLQjgvXWlI0yApGErYCpKZoRjbAlzm5q4vFjWmGlM+t+UjCTAalAlKmJ/NRUc87yHY4mYy16m7AMCHAjFCZTzMnGimi5FNpZwpIgp8ZRpibNo05NrwZC85aOTlMgmrsmmtw1pTkQuMns5ULUxZrAjhAVjSJyNVDEMqzshHNQfxWwENRJRfeDhYHfEWBZSE5neFyZW8o850zV0YQ0PZdS7D2SnYkaYrgZK6LlUtiC2v6OqOuF+ba8Z7FG3lLjXFgnNP8dkQi/I1Aybk+OgxlfoQT5HZEP48oii/Xszs6x2eb7ppVc4jkkWth31S8lTfYua3vMy2pLcuesup7UPN78y/ottpd3F9BkbtwFz2FY4zPcq7Jx3S8lzfYuddO8rLTWWXfUY3vW9Gn12h76bUBsQGxAbEBsQGxAbEBsQGxAbED8mn7agPjD+ulXVuTPIsV8WG3DLyjuXh2MD27aq8p+XR3fX9t5/tnv7FZOalkMzGDgN/xNn/jfyxipuhzV2MCUNDrR0gBjQc/Tt8eulXm7rE6fvB4H079Avvb8k7kS8ua1sSWGAOdoKloayZU3WD9IHhxy4ufQGYoHr+4Zfc6MGqWI57X/TrRIrE+IPbYvwuKo6IQGJ7x0amIIs5itBJdyrBLreAKf2qBJdOSD8vKnf467XObNsvS5P7MiErUlAoiYGtJAhANDqgjk2rZYBCIQrEXU0Lgfr+6Zpg9qiKhVXih8FoRvY5oPk+C4GZX7sBg8p6HHu/oaBGorQaZqr/nBlyK1QMzLvLMs+2TiEfY6raQBaL1jb8QCggFR2+boUlHUDBrKwRt6hqpOP3tfpNIPMyCSiFggemb+KTz0LsKm4OYdhk7CGMzE4GlguXM4ena4F4gsQu1ZVPKvlHlnWatAHAoQhwkQrdpGbRsLvhcLhZ4hKi/J5zf1jAPiMP53BYjciQOMsDiSYxmdhE3BfAP+N0qBfYztTJiNwXDMoqMhOvcSecqF9ZCPqnvvBvYgYyILdedKmXeWxdEv6Rjl1OVOj3AaTjseP1vZnJrtcrzaRm0bf8LcgGgjDHk8EK/pGXm/XPpA4DyP9RFFklaLxPqEyTTcM73xMqw4BOURjY1y3aSmuMpiYDbCKapBjUa6D2L9JNwmiOc0a0SAREqkeK3Me8vCiFDbEfHaomKYw/oeB2GT1BHRx+qtghFhbZN2ZowI5NLHdkS8pmcOjZqIuGyUEVG1SOq6yZYdwuJINL06rxXnWAyxLqDWOePRrBNFngNh7ip6+a2UeW9Zq0BEVbzJqZ2aYnJKSTwBrQPhKQev7hnIWZiaSJHQarVIKhApKi+snSYOg3IVIjS8zPJpxmJIvnPKXZNM2SqXNblrQiegyFtl3ixrAsT0rkn0ktIMCDc14VpXjxiyqQl1Cw1j9i09Q+UecnLXlFogjEAkLI66cB6hITEoH2OAolJSpl3LYpD50N3bC+PtYouoMDmG4+DXCNzri8JFvl7m7bJw382/VpSmNvkdYRQbD0TXSGPxRGBti3RUDQ7UDZwTHiPxDT0DDozpbdjvCFusTYvE+gSsEmFxtFuiO34/fjCLIf3Csj6ylp9a27uK+/QmfsVnTek39MxvL+4/+tBvA2ID4ms0bQPiizRtA+KLNG0D4os0bQPiizRtA+KLNG0D4os0bQPiyzTua5T/K1gcv4/H8TZuxkcCcWcNOHz5eq5Cq0Uh7xHrv2AstOoU4CzgKSnpE9TX8TgkCqdsCLw7Lc9eufzBnspKncAfEU6J40qoU4T8fUVr+RHuivqGPKPN0lr5zm4AUPgfyzYVfuUF5GUnl3IcW5pIPdzPVXBaFMVSI5X3Tv17lo4lQXAd6eh6qVMeR3AvFNcQT0yhRrDLs39945Ma7Y/o/t7dWvaXuNJicU4paubwg5GyzZeieLYsOrnY8TAFwqp0N4ujVkO4Cvbab9UdCP7V8waIA62XulRa1d5H4KkvoSBjbtS3mzU2Ro4rQSrYj793txbvyC63mP8b+vZ70HqZFnrxbFl0cmmOk+oEvInF4ap1WADiMAECTibyOm5fr8f7eBxTIA4NEIfrQGSZfvXva1o7GxETIM7nKRCo13RErDm54BNqQixW8hYWh9eiEO6EzuN4n9irU9TAIDuZjMDL6+7rpS6VFsSVpQMTgmN2Z5SrU9PZ9C6M08G8jwqExNL072taq2vEkvqGvEN9cj1g2huceroGrDm54BNq0oj2vIbF0WhRyBv9mm9dI4o6hZJaLjDQ6C5mj3Q/j2M8yhoWyPsCg42DOKQYc4M0TH9Qq47k1gGhNurf17SW1lssjIyTnxOkNlA2mN4drTm52L8n6aUZx+hOFgdNBuociNgCYU4mpmvzGh6HEgbaxTqXERErEMwfmQKBMxNdL3cZiDX1DS49+x7IxbRkBsSyk4sHAqscvYGr0GpR3HXXZJoZ4/Wb2g65zeOwa0329smV39w1KX+kvWsCg8P+vqa1tN5i+dzcNaUCRJoBsejk4oHgNaLMmq/hKrRaFO3vCAAx+x1hXibibwIdwft5HOymYmwI/gSqGNYj+x0BB0jhj/jfEcrgsL+vaS3RuvoGl5OltfgdkaKuJJdjngKx7ORix1GTyZbojt+aH86sSL+0tFe29pPrsFKTu6v264D4lc3/xeXfU5PfXLX/5EO/DYivV/4GxBcpfwPii5S/AfFFyt+A+CLlb0B8kfI3IL5I+RsQX6T8r12dv7LOd9V3A+JX1Fcf/i3xGCyCZI16C5MBr9I37IXySCU1hdmHtiZIkyNUi0tk152D9HiwDI4Ge05Dytq7xHTuLPOkwHPcmmKpF7CP44J409ueq6aFuvszuKXd9d6PzYWTuecWeQwWUzV97rcwGZSW0LAXEKgk90qxr1RoampAQD3fQxfIW3/gLU4V1cADbIk016y9RwxaljVqYmlC4/kSm/oU7fGEVsUJEK05CKEaC9ylJtdY9/HFLm1Y4jEYy2ARiDuZDApEw15ogaC7gThMn056INDpuBKkztXnaApEIDMHWQOC5kBEezEYrboDCLoGBC0BERWIKY+hjghU/C1MBo4tz9gLdBnqa7bmr6LDWl7b7Yqc+0H+zWpacDBrev6P4P+Cxh7INAKI37nONiLwum+T7xiQshEBmpu5rAQTdE8d2VkHfREXI+JQgLCaSZ8VNxlOb2fEUibXmFNfBks3ToqE87KK17sRMecx1DUCn97CZCAxeGnZCwiTQ/YAjhHjGT1JnCyVN/7RmF41BOAwhG/RLoOeDTAARI/1gy8cJqVkWyMsMOry7QMVTwqdOuGyEtALMJ/Rs3p9NR2xN/FIyb5m3GfMSBEjkoFEh0bPGEy94Di2m+t8Mo8IVpLi83ue0hHEZSB0jVgKn2Og2Yh4G5Ph0E/ZC0FUMiTMeKln2XQlL4QT/E6iyvufzLwsdjppdupLAT8XGxGYeXtxVbG92qUl3xotRq4dmctKULKXAEFRR7cfEeaRUmuG/eYm013qGcixQ0hVAAAQ2h69LE4OiHF00Q0gdI14E5NhvIqm7AXWmCkaFUtAsMMK+51E5WGclFUx7hh0foYGTbJZ3mluRBiA2F4PBJxbKhDycSB1WVGbHDGfaYDoyQX9KxBYkCCdEgvXsdczkKO0ROaXMiJoFYix52iZxzAB4k1MhjRnL0DsR68w81dRINqpKRZVDaLC1IhUpyaeaMS5JSXV00geCPWHmU5N2JCrTk3R3FnGz9naj7y688Jdk2mCjGsEM1LMTQZrBJ8RhdGFVIehAKHtMQuf8xl56RqxxmMwIKyT3sJkqNyJwl5IOUKhAiW2i/XRpQzq68LWTNWRBbkdwQRhdl/5HTEeyShPgYjwh2nyPXd215bw68ZcVo5lWT+WszDz43eEfNPfEaE4zoibS3WTEeYK7qOkbmoJx3WWPtIlHj4zfGNAoh2INSLFG7/61u6/vsaWZreO139Z36tbWfKP95ZeWM7xWpqbffyuRF9s+8g6X3eLSQtALDvFbED8LfXdgNiA2ID4kxu1AbEBsQGxAbEBsQGxAbEB8Z+v7x95UXx4rcMn5vHZviV/DBDmPZJF6b3wMS7j/qxvwl3gQYJnrPPnYGm2p5u6AfOfPE9HFuT03+c5RmtSosqPQBo8D74Gcefr0NTpBtPCp4j1b2pbRbPX36LfAyCKZ8tST9lf8C4soF4iCxTlIbnpcHOHgUsRVoCYGHM3whJhJR01L8LaWQihuMC/AyLUN0WRp3uLs2Ya2lwn5VrO18ZiVD39ORBxArTv9tgAEQsQMS6VMKkjeBcTIIq4CDgZAOKgQIRFIGgBCFoAYqE205Y4IOYjYgGIuJzp+4CgFSDCW4CgO4E4XAHiUEwGlEuRObxinIocjanA7AR9yTYhUgW+xWEofAuXTvkYaunR4TsHWgbjSXCoxnMeXl4yK3mwE8rguBQKBKdRbggHgghi7CCnISxklveHAeeKp8pgdT7IJ4StNJikQMi+s4ipI4SVW87GlKmBVCiHwz4IaB1U5qiWZ7Xg86VN4F2I+YVUjmdcMBbUnuaMOTgYlyLbdNFHcBGYqUA6rhBUtGhvkMhu4Vu4dBI4lJAh8u4tBKo8CYxmz3lgIFQpwHMpipAQuEcxgmamijWxhFlNsyAKS6XIqVhYtFdvIorG7QgFCAltJghNjJeHDNfK2UC7KlMDoV+UI4FQ4XVIe5PVyteCRMVHeBHotlM8kB8REMgBJ6MMZeZSCCzdBQHUk1abeQvJNGJ01emUlmB8C59O+BgXew8fPIug/IhIYEe0ofaXF9POsDQN4yrhJXguIZYYd6d5J0IdknI7jDBwvMSyF+Sd7mLcDgPihFfrU2GB5JazMWVqFLciBaK7mH8FZRxta4ELdkxlGY4Z5aWpqS7W4FJYpyKVAVGoAA4ITtvlVFh1Pl205vcGhLIzqEo2rAFR0rg1ooOjD4zLqj7HQNRIbk2AUMaGAyJF43ZUICQ2n5NTtvKcjSlTYwoEWhtN6mgBCB1b1jXnc0r+rgm8hRYI5TZkKkQT5iJIRfrg2LFuaqLKnqjpAjqyT72JFCU/NQlAccJ5eHkBp1CBjM7hK9XO5BJidaorUxPqoGliASJGIscdGUvojdthQIg7C9Q/eltkPWdjytSwqQnfRiB6AyI2WmtWC6wRrE8Sj8WF5Fj4GDLQsyrzqQYXmAkRxKwzmBLcCVwR5i2YIoZwGM7Gn2DOArjZNV2XI9xEzrh5kH1gZwzKkziLk6LjPIxNSnC18iwP6G7oTUKU2o1nVn0OY4CgDkgVhFuCnCLZXvQBasCcCzAZZZ/6nqDbE7WcjSlTwxZrLkcW67M5uogDY5zWArolxmWhfx3t/d8LXd0S3b8t8yDCjeNL2/XfqPfn87Ztzs5IZW96c61f359v3q7zIG4fv7dJ9+fzMe3wnI305lpbXn/Utj3025r0n6v1BsTWpK3WGxBbk7Zab0BsQGy13oD4eyv5tRv5C2r3UZyOPxWIcFeKt9YuVV4E3nZr1C9i888aH8NnNvvUpIgcXkEY0cqEg7TWwdLrOZWnETU9P26bNzUtMDPStB7oKsnxbRojlXeyygPJVjvUvFvvrdnDyuzC8QipxDm7IjSNCzeBCDMgvP7Gy4udpE/oU805NEBY02OVZUhLgfm0yjFJ7motEiFv1Bip7QiTXL2Wh73d6tPFBSCC69cZEGNHpOYiWgTiXSNiAYgUG4hXgNAAT34tEC2Lw5vkvFZjpGWGtJoeNAUi3gKifAEbIhReBEcxugyLIs+uEK+RbOcs8zFUqD0dTHGDjFlByufQHM8tEKQdYSwHfm/f8SaUOSFhFOGQwBpHB/9wKK8FKx9CyzCNDDBLwD0RDQKZjJDPWzRGhBmifiw0cYIxTxZyrxlLVHMoLxpru7SHznymqJL0aixgvAh+1M6Zw52qsiskdqlArPMxMG3oO/m4iqK9Em7nWDjRAZEsStbrK+oo016Gp2xRYHBIxnRyPjdZaBvqSmH6HlFfqVf3I/5Pz+Mppo6It2mMkGiWwH2CGieY6slitSsAF90P44Oo5k0f9GV9ZUOEwouAfktKirYSCDh0WoFY52OIthIzNySAiDMRXK3nGNOhAhGyF1kwnxSnniGjUzgXGeXXESFhyEvSWhjHQ+wLcFWJ+scRi/6FPa5imTLe6pYSih8LTQxIqoSGjQgF+GK6H8YHqUIYBYhka4RyKCKpGYsDIik7ogKxxMeQO61BRBkGA0KYEoM/x5pTgYjkgUjaIMfhy5KvX6yzB6IYPMXC8eBw/KDNhc8KVooGiLe6pQSqI+IOIHq9aRGSgfFBloAAl6ssl7KwpUgtu8KYDTo1rfAxmqlJp7aomhtVo4JBE6UPJa0owSQZy6FOTcqlsKkplsU6hcIrjVq/1Pf14ohYtYoSSHJTk8wQquPxZrcU82PxSiatJ4sd687B6X4YH8RJFxmdBr8bjBfBwkBjJRPmvMquUN5BxmK9xseQRQjL8hB1IY0qkFXOwWKV+XdEl/k/vpvn3xHGcjhqmayeIcd1sbYayKKbjWkdyVgWB+V4SInK8kim/pFNYUM8UzKmy7e6pVQ/Fq9kUrVMrHas5EGmOiK6H8YH6QrDJWitF7b1347rvpuv5VHgmgk3yMDz46E5/8oWr9c8raS7f0s3W7fUM5/NN3kDj+KTH3H8ejualdq1PfPZfJOP78hPP/9rt24DYgNiA2IDYgNiA2IDYgNiA2IDYgNiA+IP2v68qv+RnR1WPn+tZoWrNfzFNb6mb3HXuZdQPR3qluefW42KpWYhC37umVeeHS35sCxFzdN6ne2DVwbJa3Xxia3G8+dHmjbVFqa1EtdrN9e3iM2/cVKhMD2X44FxuTHt1mpUeCCspFRyzVeFHtr01WwjOiDCynPAwryYWYq07U2zMVONTqYMGS01x9jWI6yUuA5EvB8IX/2E/yT2fAcQdB8Q/SuACKtA0N1A0BUgaAkIWgViVo9VIJg3IcPlDMZFBULZF0NKx6pQATYHVd6EKV9wkKcCcYhQweCXYw+DaVYYw8K+RaoKGtCtcLkOVXHjoECYakZ1SzFFDE7J9TxILey8ThUvlG1C1f3EPFGghzFV0VDxntJeTsk5yKvGdn6m6jhDxVnlbGeBGxNV76PUCAEuV2IW7so4A/VRHU04jmhaEeWlciLMM4hQyU6Mtlhf2w7KlbARIRGtswZak7E4LD//TfI0Bx/RrfAvgzvFjfMxn5xqBgKxfHYNjkqkFboWTqkjKo8E7/dHNaCKxRNF9TCcigZpLNpYF1IXjvOCf5KstQCijAgLCCeEhnu15EELa40s+GwlSqSPa5GrkUYyJYnW1oh5B1G1NaB0IS+JXzrlVZjggpqRAYoLotJH4Sxo84v8jwpOydn4H9ofrFtRc/W+KhzpPbmAO/MyWt8ToRAkV1KsfiaquCFAyMv66oKCTmOaRHepKhrggmBJtfbCo8Ji31D9aEbEpc0TnjHGjYmuRsx8gvaGlagaHjV01wnXr58BkWJp4IBslQUxgFdRlC8GcmsEFmv2KBF9jN4DgZRRCDfJWCDjX9GtKLl6X5Wi+lHZHhPfkzkQiQyIRBWIwieh2mmBWhUNGxG1vRgRfAV3WakOk8Xa8mTtkeh6wAORsDqAI1VKVA0PNyIkcVkjEpGxLzBpGQsBCj7KmxCxm1CIIeSILQJEjFIxzL6xsD7sW2w8V1i3ouY6Vdw4nxubmYnvCSYQTs9rRD0vRm/6VKhuzTQSCL4oqqLRF3ZGaS/WCGGGUG2tc5ypziqlTMmtAqE1qrwYK5EZhVz3OiJk6UxU9S2MfVFcQ4SFEOE8CDYCq7iY4sYAngRc4PA7AstSLLcInCbqXXfUEmNhWrBuRc218VXhyShV1Yxu5nsSVIUDU2RV6giq0AHGt7mfWMvkIhHGiFPROBd2RmlvZ1Y4seiLCJ+k/I5wfQRFDgYNp6D0wpEZzBPGSuTbmejvUm/rStza1n+Npng7zTveKFghQL91S6v7Kp8RbVl4MeD9lIXbuhLvzfvW9lYgPpoXkVb3dRP+0bzGX5Cj8fpte+i3Nes/U+MNiK1ZW403IDYgthpvQPwN2wbE1qyvWeM/Fec/9/pcqfl7GhQ+IeVnABF+UZ3eUfMo4Y7WcSRdzabzabI/kuW5a5ofsB1tMLx2yOByHybVW9iSNkeeyC7yOjpXk+XqyDuxvDeFvFJiXHuil+ad6I9Gq1Wnz171XbxkvSdAxMkVwc8UE7VGF2kRMPvb2nT4IPociBUGwHR3bJuxKJ4wy2aMDsS5QUdpS7zG5PCX0sSspoo4xOW3+aLTHlkEIoUqXWGBrWRdEpaAoA8GIjkg0muAoNcBEQwIWgaCChB0BxBpjaYSl6eueGtEZKuVBc00CtQCYVoW4DmMoYrMQADFyjHoiuIF+BcpHguXIqiPCiaRo2lMjMeiqniw+HmkygDRV4rJLOcRELoU9gMcSC6SllxAxdxVRLHjbKlJtToYCHNKyREsDeFeJPFhOXsmhz3SNlYIUqJjEHiy1mo58toyl2PuKZzrQUI7hWsiSiOH8vqutfJA3SIQobBQXl4OA/gPGpiEBse4RmQAUTkG4CUgnfAiUnV5kPC9pNGLgc9I6uMgKh78xnhQlxG8dq5hqF4sMoYxZKieJOA6QE0DvA0JRSPAihfGYwbXJFFJjUud1S/MjoA9xAr3AnYMfcPkoBpMjYXnYUDU/0hbAPsEmWu1ppxrHxFS7rVfeI9pjdRW9uUlZY6AQhvKwqbI+eVFWB+RwEWQjkvtiDCOQSe8hASmAqtWZNHg0KmJdTCOhZ3BDAWwN3iYw4+E8+00fivWahd0BkKgnDe7sxgbBLIOp1jTQvED4J/MYuBi3BG4nPCIMCBOGpM2lQJ79z8UFgnOBWNEZRsUCOaIiLYH2UvxeBU+6aKnNYWCU/KcEYAIdkZt5aG+LR6LJIWPX8PuI4JHQQCCWQpzIMZZbuiVPYFQfoC+06AjAuoYg86YHAkW9gZfzbAEYS0DzgMsuKAaaMKwyLqIEYAAe8OASIX7kQrRAEB0GKWDzbPcnCkQ/SIQyjeJ0OqYAsEXD7gozCih4sdirixcE+kVMTYRqoIyNHqjUZiiiLUyLwNB2YgEGBHF6UNGBLVAGMeA+R3gWCh5JMXCpVDXk1SIY712m8oHydQklLTDAM6Hn5oEiJ4/ncp04IBwZDeoeUBJSebb3jiEcDnxQAjnQ9YIPzVVJoebmgoQqQIxmZr0Wu8ViFiBSFQZGpjvUzM1haW7JrK7JmOhyBoBZw8skfAYYSBYI8L8RKDXkXRJLWJQg3EpjiqchaUJzAc9FnWpO2PRTXw0t4t1wL9nqF/gSFSPElIfEskV+cryrr4k5+Qi7dyc6pSCWwDefxjQFnjAGJOjLtaRjGmhC3+aL9Z6a3IGQ6nodGhtC9eEjsYxKYs15LnMnQV58rSH3xHGQsF9Kk1/fCz/Qk30mm3mQBLrPf3HbClO67QktvjZm29nWr2xvpXHAhCM7EcAMWUzfDxHBNfg7wbCtzO9OY8FIJYb9Gds20O/DYgNiL+h3hsQGxAbEBsQGxAbEBsQGxAbEH97zf/QZizX9E31f/+TovCGI9fz+4uBSOUT3iar38fQSJw/VUkTUkS+xd5QdQ32y3b5Z/U5yZbQyukWSBflKCsqu/1rn7HNJUJansf00NobQGllX1fcYCZP1xL4MfolmsnDGD4a4AJ/o5j6mq/fNTXMiMDHfbvB3lB1jTTJX31OkumTpHLVz3PJpRYvL6HxNvF/A815Komm9h/Ow6WtqSMSVJpElZqgplbJReJmPTeQfwsV8Ri8bkpvBSLPWQtB33uu1bzG3uCq9pBFn+RvIRu7cpPLn2bdl8rQXpMgCotlLwFBa0DMuru0kKYQFUAXgAipAaKq96RkQFQ/EguFMEvCPkOkXdQkLojAqvdKcS6BLkV8BXuDgyUHB0TxcpH3vA8mDM8BKbOBIp9Lp9LvB/V0YSA6AotEomSJFTMOA97zr9oYpqBxEPZJjl1hb1Seh1fSwAVgqeQMDSfVHjDtDWjvg62hr+2WlvHZMtazKRUcKp0mAwhmccQU2+BgNhcSMCREz8LjH6koYBjvAj4ld7I35Ophd5LEfiWVBYG3/uWzyFeYnQFp+LNX/Y2kChq9erpgasJwl5olMCyqnwNqGVVBA1YKOZqDSxtMtdQAovq88BmhBJWtB5hLEs2xSGPTCJV2pWXKOREgMCJ6OG1kju4dZI3ozQbjqCoYvNQYm0J1Vy7N69iqLxFUK0Nfbs88Ju5nb0Bdg+UhDlRZEKS5MaeDc6OMUDv7sNRcQCJgeqTpcBgQhUmh8AOITmupzi6ZVJ3jFPFqfksv6JwbC6YrS8VnBErq2GI9wFwSqHCMtdc1wnK1liUHhB8RmMR64XCk6kcCFQxeVIxNkUqzscZrJ2a9AgbMgapLk17D3jD+BpxOiqsLee2ZpLoIyp5wuajuRWqBiJI3RoQoZhj9cjC1D+ZvmEwFd2uqxjYFCKQ2dgjfg1gqAyKRAUFD1d4gPyLIA2F2PfM1Qnkd0Y+IZmpilkS1IzOVCbd+O00YYXeonM697A3zuxIgUmVBlDVC9HuEdFasYSyXIkBSrJ0cEMakSIkqEEUbo0whmKaslbOpKaJNQe+aCrNkPKNMTdXASfsHa4SfmqqWCBbmMLtrMjkZWyNoulinwmOQqUkULvxNQlBWAxgNCQoe8X72htG2bBCHwurA2DxmUao4U4TTCrMwLBcs1nBMEUmrCH+roxqfMZMCCyhWuGPVxhiMrSFiWUIRhYOL53loasLyCzcwpAoqIGa8DRPhUlUNqmwNY3FYy1SXJE9/R/D8YYJhid69pcnvA1r4nprbynQ1t3RnSXXzv0zTYi3e0qb7fudXbZ9b25J/aSj1v33+K4F4L3vjY4B4ryLG/fV9XUnrjzgS/QXbf/Kh3wbEBsQGxAbEBsQGxAbEBsQGxAbEBsQGxN8AQ9uQa816e5P/gs4Kn3zW3wPE5O3/JH9zbJ6iQL58fur4GFGfVaq+hJ09hl5UnpvyUd+DwxvGIf8rkQ08p+Ry9PuYbgwcmWt0vhlP1/C7ALEb/5Ntr1328pP/PmrSR/n0+BMpf8gxO6P5RD+eX34+7Tgf/L8AxH5ao93+x9N+9yT/jYd3D08/nvdjKY/Pj89ckx/jv+Ned2KbxUNtUI2ax9KVjXpx0aOYqFHkkM2mHrEFO3t8LA5yTK5vhkJtnj3WBSF5Ut/4qIv/uuU0YUkk+7QIxB6dyXi8vOz21riH0lT+9GMPIPbYv3tYAII7rAXiYQrEk5VUgRg3B8T4fTf+R/wXZY+f9hWInX7arQERVoAgLwzSbBJGJP/ucAEiaURvCkTtePk89O33AH5HpAW6yhUgntC23dioG0CM/bOn9REhV+7+6ohYAmLs8ufvz9/lTAZh952BkBwECNo/3QSi8BDO3oski3OAsjn4RdmkL+IW9gbPHTIiGiDw5vN4LGNEFAX9FSA4TtYCcWgkQw6q90ESyDlq2fwGtbxsfAYQY1OexuG///FMTy8vPPU8EE8M3ET8W4B4oqf9OFF9e378B3/HFP/s9z+ffz7vMbHQbuzq8cgz/7/fj9Pa+Onl5fEf7HuQYz+ef8i3b89cGnf9P+PGZwKIbxMgvhHO43N+POPTWFv6KbXjvxwgVHcP70VyAjnJOq5ntgKmJlPjYCBkroFthepL2NlhwBrB50B7Qmb/AfnxenCUKNqpfMfRAH5HAaJXvQ9hBMgn+Jr0EozlkCQDsdPhzz1pI4K740GucZ6EBJI9Q/ZAP8arc7xiv8tfvobHK/l/u//JRPJDQfsuVzVhStntdi8ve55zxn18Fu14RPD+7zuMo/2OcxC4f47rw/P+nx9PWCMAxHeS0vjM8Vz9NJb2P6mllA3OhPmJGKNi7MqsChhqUSNcClLugkqWYPJOEEUpV73AGMv6wpwFqk4tHCLFKn4gAwIj4iDuJszvqEAkB0RXzDsYFONHYETsDIgyIgoQ4yKNzn1gkGRisKP7nU4mBYi9TmO82O53jz8VCBIgpJP5++NPzufHTy51CgSf+Y3B1fmrjghZwH8CQnyiEYJHKkB4PxFjVJzMB0SYD2i6LtbDDSB6D4SsEUybKUCUjhcuQ/ZTlRJk/BrhgaChBSJRKmvEXoGoa0QFYr+rQGDCqEA8PjkgvnsgsNjungyIxyeGcBwBT3w9A1B0KYDgpVrP3O2f+HwPhKwRkieAwCcGgp4NiN7cPWRqqkwOg0WnJhwP5bo/n8m6yt019WJgAbWMAgRPX2dzanFApMldUypAyKSmbA+4S2FqKpZLSaemoHdNTwBit5sDIZOQAfEk6/qD7pXuKlMTrxcGxHeZ6fffKxDo9m/jKvTtedy/2wkcOiL2EyD2i3dNyJF4QpNPOyI3NZkbCBbroiuRQKQy/w/+j5fKY1Xj0NsXqGXY7wihhVCqLOoExoYaqYkjV1DqGueiqhbyOyKpD1fxcyGUFKQ+otuhi7Uof5xRZ/yOeOJF+Z+HZ/sdgcWal0Pe/0M+VSD4Pp/3jn85xT+P/8itp5uaZKEe04wryvhJRsT4d9zHvU5c0oPsBxC8HjRA7OrvCOSg42+syzhqsViPeYwz6Y4X6yepB7nfCx+wrTAo3sCrSP5zWk4T3vnLer9b/jw/U699WvhF59P9vP79F26f4ykiLKOVLnoPEP5KbK7K2Zk4eguI//C2PfTbgNi2DYgNiG3bgNiAoA2IDYgNiI86cwNiA+LPg3L58+vPvn58A/MPAQJh930Nwj64/T7A/tgeL0FXfN8jl7JNv9f0s5jxtELlrMfZscfZPn6G+fiKjttPcuMnq1MOSLPFhU+O+9KeIU+MJ0Dg+TReZYTftwQUBkuRfPc8PLVA7JTl4Dv9YRKSl0QrQOya77tVIHYLSNVcHmaIPdTS7ZwdNfsehEKwgH8B4qHJjUNJRj3YSWdN+jq2f+ZAxFTPCTQVvZC3qOV91pcXcF6UphEXgRjjTDMgaAYEvQ+I/RoQD+8Bguhqx8+BoDkQT0j3sAREWgTCUqXXACEv9Murvj4zPXkMi3D44qlyGTio8kNZDsY94OjVA9gPzb4f8v1ROQ/IESwH8Buw/4ekp6ef8i/HiflsMBokxVOTDvUqOSPNGAl+5jjXQzkqx35yuAbptDVcJzLOBafguuz3elx4GU1uTxzzrhwQMboawB6Rl3lzUQFxvi/Yy0dNTQDeLEHeFTevmYO8vIu3qG1E8LkAQnxhqvLHGCjcCTencBkQHATLYRzowjjgeO6DMhH8vh1pEF7Cf+hCsByM37CTb/sSEuSI7thkPnv3fQcuxEOTTq9cC+9rGglfFiCkfOVdkKaT/XtMTWNz+EzZyxnv9yXMLxwOl9sTUwoqB0QuWXlpvwSKTQXE+b7Yi/T1JXYS1YSgSh6ItEd5nb0belErwRoxphnAbVF3C1P+2CsFsXIZHmQJQzc8CFvh8WeNvv746fch3Y+f+6YLHyu/YUzB38b04ycBgn7wlQmOxA5ciDadLaRcEue800D73gMxpuYUJOc9ED57IAAhUow57H8oCfOHtMLlJiOickAQxoWyCDxjigqI832JotqR1CxORsSl+EyIkolaHEAEiHmPeI1XnFPMh4Lg3mJA7KkBYhwJ9OyBmITBye/TdOSB+K5XsfAbtBvHMsZ4L4AYI/2FD7dTtqRPp0g878F/0CNMZfFAjKWNKUBpeZDU0tFPAOLxSceSpGAg9parjIiaG9aIygGREaHmHomgT2LqOtX3JZnGR5V1sBEBu5NBiYjQJxPFniUgzP5Jp6YH8lwGTBRgOShj6LtNTZgw6r5dmUAABNNTwHKwqYAnuZ0cfbKpCaSu8b9vz+BCcLfVdMjFTU27dkTo0e/lUgAfA9QYvkxwaRSWBc9rPEKMOsNrXM0Nd02VA6JTk2pspJipqIA435esYjG5AtFjjehU4yOqsRS0Z1IBAuOmAFHVOnjR5CpWLgMvXQ/PYDlwp8vCKgszeA9+30NZUpURgWmD+RQ7LI47+cYpnoSt8ChH+Ozx9oDAhXhs0iGXx7J4I41MecLH0KP/IC2fh3TY/2NM/0Prs5M49IPkbceFh+dyw++IlgMiuh3gtgyZigqI833JSDPk8suD/VuS6FtByYTVRyJBaYN/PQAIEwIzILqh+stMbv/WuAxLfIT5vtcF1/cLt5R0M4f9lf11nbp2XpvD7V/W6Upt6hlmheVvZe/9Zf3vZbbrGpfhdd37kSnv3erN7/3lfiwQUxbL9qzp7m176LcBsW0bEBsQ27YBsQHxxrw3IN549gbEBsQGxG8E4p4MvxL699fFUn5u7T8s9w2IX1UffW6VyD85OdqnC3gJiLya4i81kgDHjDTgKxQNj7ReZI2kW/w3pmt1c1IdcdVBN+HlVtQFcbGF+kZfa+sqCZKmpvarW3frxbTUthOROjyRvZ5/rgYYKU56AA7wEhiRh7lJbUH0vWh9BThryLzod8jJeb3I8v5eBgDyvnCj1xKaujkg9EX4uWkH5aD6w5Bln9c3uFNQawMCwZ2m9rTGrAkzICbpc9tOZXPESe+sAxFo6q3igQjJNawp7nOAaOuGlxulo7N3cynmMgUIq2+ilfquAoEWfgQQXVM/ZXNMgQCLgF1U7NXZYJXMog+gR81CHp08xqykmuLKgDAKiUIGwU0CaQ56TYHTEOgwoISDsCFK6Wc4nUDan8g4EPYyMTxdOhFTt7pBYr3LYlBA9fjY6PE/1FnS5k7rgmb7+rJDCkoXUaQ8BSImX/uLiLRbaEdedb7gm3mxgMfhGRzHAbr7KR+U/4H6YES0vUPwLshwDbEX37OsCYmzt6Oio8FOIQReQiojQt6OZoGfxDFbdnESJ6wBMVxSVxP+t4eFAH815QJ1YbHrM+jUrgc1nMg7gtoLGBAIOcL0ph6HPQC0QgBE0Poehml9M87p8dp9C4REzpKv/QmeC+rxEuuSFs3wwLtfoC+jGRzkXlrMpYSyRrS9IxyFTrgJ5pDiR4QE/uTowfQ6MsLhqYwICbWnoKoCIduIsDB5kFRUbGa4QZynlC4uLB01QMjeCkQndjpBDTeQMmbmP9jEUI9Xc7WTAkHKo0CzfX1PhXNxilKDyYgwIFB7A0J8Zy7Vacs5SURVCFEGB59n3ivoRfaBoRK7bntHOAoIiUN7Y0TXrREU7SgrajDyQRuGNV+uMHFCGOdqBSLQFAidxXMvQHTiyXJS6w45F0wf03eya0SBYEYEOrrL5UYiF139XIHoclHwYD0Qt1iThexdfT0QUoMJEMpR1dobEMzrMKueJSASGRBJLiDuz3RGKamYF3ggtHdk/uyhniHddG7vmuyo2Hucy2IdyU1NQjxRIGRmzXpN1cEnY+kwRDWWSf0JLiTqwlIXWAirRJFFgZcLn61XvCzAfF7MNjXJXVM5Hs3ThLVCBIiuqYuvrzqk9KYaxbV2y7u20Gp/wpSqHi82N7VTk/0XtS/5A/cntEOgxNPlls1Rekc0+c5mtsJOJe3vCDsqXAM+cPF32c7hJDGR5ChGN/gdYXwFuJrgVida+nMWQyVzYfGMXLiUsNpA0MU6DPDb4lKg0RHIFmuK/nhil5KsWiEyKDvydfH15cUXmh689GutZbH3LbTaj/U9m6MLPlG5sbDFOhUGB/qS68X9Ce2QDFeXCZuj9E69RVUmwdQTZXZftvTLcKrAcS0IP3UhWfM/Cb5WLmVqbgantZjtu6u+VmsbEfTKbcrCqN9nHjPruaflm+BXAbHOXZjntey6cs92K+Xy8Xvq26b8yEcc85K2Z013p9we+m1AbEBsQGxAbEBsQGxAbEBsQGxAbED8pUB89cp8Nf7QnFDzhWq4AbG8vUYeN5U/i0fjPLPkGRmIGuP5ZhhaHkOihhPRPBxM1+pkthex5o1oAkVfa7cjUlzKNS32SpqlaOuUFvOw1tSnvxUIxG0mPI5p8HsNFeNRXO2WPH36GQshwIAozIoYJzyGhGensa3VMhCBmvpn/FvzxmcvFo+vwQERm1yrTUhJQ3FSdrwJRHR70ZoaD3Gh2LEfOprzOO4Cgj4MCAu9pBkQzWXxmhGBjo7TsM4SEAHZpXUgaBGI8GogaBWIg9g0NO2HY0rlJIBT4Lkd2Cvp8CZx0Y9YYYBkCSgVTQowM2pKY1ZUIFyZBE7EwdUK3AgO3sxrYymDtaPkjTgjGCNWaykni7x1BkHC2lG5F1Jr6GSY14xyM7zjTFsncEpw5Oj7RlqzBESQqGTD47BQn07mylDw3A5nSaNqE6YfscIAyRLeTMZoCJP8jFnRFbpNKmVKoLNnc4JaK+V+ZIQU29pYymDtKHkDCLy2brWWtmWU6f1jart7UeFgdoe84B5hG9O719RxxqROufZJzdNas7xGwE/Jtb9TTwgiz1Dw3A7Zq+lgOmCyBSsMEL5yLszdAaMhTPIzZkUqdBvwGBC7NoeKWitwP46aqq2NpQzWjpJ30AvApE1K23JZIop/TG23kBkuVgdhQV3gA9PZxCNntHU6wvZKgr8lRWnN+ohoeRxDUHkOcgwFz+2AHoU6q0TKTj9ikQECSkzIvTEacpuyLqip4TF4IPpYa6WdaYy+tjaWMmNPzdsD0ctxtE15HjKDm39MbTfSF7sSTQGKKbkz2joJw4VH0eBSvAIIyWU6NfFgDg23w08GAkTRj1hmgKD4mA6DMhpym3LKrDAeg3Uf10aUj+rUpOY6ytXwtbGUsMXpa946JTJLRDSSatt4rs8gbZV2GDNDyuoViF6kgHorUdgj0eCvdTK6GxzHfJ42NVUgLCYOtkvTfmMhyNSkDIUue26HLY9H4SlQqvoRywwQZg0Jo4GU0ZBmKWmJ5dGJ8xZqwzRKq9VBWBcJHIg8qY2lTGhHzVunRGGJjBVI1jawQABEKO0wZoaUdT5KzvBwEZZHxO1BPcPXCb4vcKj0eVpr/BphLBG4UM54HEu/GOwmbEElwh2ZpHJeW2s/c/D5+q/ffy/p+m+ZKymX8k5Xvt2/pTce97+s38ASSW9JFe9t/q2qpHfU8xYQn7XdA8R9rf+F2/as6ctV7s/KewPii+S9AfFF8t6A+CJ5b0B8kbw3IL5I3hsQXyTvDYgvkvcfAsTX6oLfT4q5T27lE+q5AfFJQLxFZQL8hCk/g34LU2SRK/FhXBHkru/mLbJFfJr1J2CsRFCevn6cygT4CVN+xu9hiqAuFn75aK6IMjGusEV8mmr4ESfckDFeUfvj41QmFvgJC33/a5giqAulz+GKaEvjOlvEp7kOhHz+WJUJDeNP+Bm/hymiMbD8OVyRUOLla2yRkqYP1WdiOBZuiHmusFIIfbTKREsUsUjw72GKtEB8NFdESQvDOlukpDl75xVw21Jp0ctLj4vnY1UmWqKIMS9+D1MkaAD/c7gifkQss0U8SaA6r0gwWdpnYWPCxfOxKhO+ekVn4jcxRVogPporMgdiyhZpgTDnlVi4Iea5ord5H6syscBP+G1MEdTF7uA+miviW7rMFrE0ZvUE55VYuCHec4U+WmViiZ/wu5giqAuA+HiuiG/pMlukpMnBOa+Eyg3J1XOFbm0fojKx8oshrZTRljZJ9UFMkY/nisxrc+uXdVg98qrt/SoT6S2pPowp8tFckXmq7VnTL6jN9tBvA+J3N30D4os0fQPiizR9A+KLNH0D4os0fQPiizR9A+KLNH0D4i/qvr+7dhsQH7EFBFHbB3g3QsoRL1y23ydbWvmWrpSSCEwQ9WKZ12u5jnG5TFqu00KO6UYfJQfEGPYZrtYtqmPLldaG2UndNSDCZIdjNOSQKdY98r2krFyJeVPirGo1qB8ViDHuBqZEqVdY6v+qJz7TzAjOqiO6s+OkQ2JTJ28HEhdqr68Oa6in9tmcXxHj9daGpeEQ8NJxWrnaFndI0C9Ovs+AoLuAoBkQYkiQFutFtEI8mYuXVCAmV2IDRFgEYtpePyIUCFe3BSButHYNiGysCGYWiK7EWfQlCJwI3iv8B0KgBiNAeAsE1kMdEZ6joB4nJOb0qZsrVZxN98L8U8BpCPkAAQet10JN4MOi+U81MyTiPJiQ+1FrSQoNpz4I74Tz+n97V5PjuM1E2YQbILRygJk94ZXgU/QH5AC9yNxH6JWPoaWRU36qelWsov5s93RPehJqkhlbosgin0Tarqf3vL4G/kVM58LfcHySIIyL5MZM2kZM1xFjl+Tc7d5ir3JT2KkiFl8eJB2R/Ayw5mBOBBKjQQkAoCBkSXfCNIDXCB5Ox1EQjxMyZkpQq6iVKnLfJxAN9I7omdNAmUZNh3Jci0hwttY/18zQqSnyPk58ctYOHjF4jP2NrT1SpcjB5IvUSwpT+RvGJ4GKQE/q/C62ANLBGLhWjB17Buz2FnsHYYgIEDrddfAbyXBOoYNn5kQg3c1XGJWSSUi7eL76W9NzFMTjZMqJd9dzgJqFKlWI3ZFkhc2243zlzyViWoN2l5HgbK1/qZmhQHTC8KAo9Y5ALRRT3S4m1Y4VNRQIEAuUxhCFcQGgNDbqsbIziM2iufW029uOR/BNNQoARJnuRmZnDMIvYBulN1yHeoWNoaKfRC7tgfAcBfFACerHMleqyHL1+QGR9CLS+6qYsYgkK7UBZiALzQxdrNXag6LUNUKBUJaWxSSRs7uKAWF8klgYFxYbeVVgJZBawxyIjd6OBkS5I2S6kxDZq6SX7qfL5TSCTqZTU2Fj9NLFVAFRcRQQxuUyvc8yNVXqF8IWnM4wnQv4jSQX1zKSLEQz6eZMMwNeLAwEH0GUul5iIntLOOb1NRLrcAi7IhmjyWpVxoUbM6GWYTx0aipAbPRWpyYwTsoaoZWqggUWnMi8h2kA2dUqjsWMTNkYF3IxicG5oYSaoyAeKHT7XsABqZUqpr28uE/hOZ0L+I14IJaR4Gytf66ZwUwSWq8yjiDKEIQHfqFaiLlSt0v/EkWIXp/5LPA3jE9yHpVxUY3ZRXxfLlxrWaxT2O6tLtZYuzKrf4RbWw4/s+V0X932avu7a364/jvie7D8PLpH2s8/F8OjodbbPl8n39HV/dL3u7F8zDaP7pH284P7/+Gt/ejXuvrbR9eAaEC06BoQDYgWXetqA6JF17r6G0X3RcN7LKovqBzzEV2Ou+XjPxTVPw9EvLVHFQTwG9Z5cAVXVXq78ncOS24DPf6OctkqDW7P4FvY3LbYGW5v3h3aOLLf/F0sFa/bXfcm+5T7RjxdrTtSIuzm5YdFH4eqRKz+zW78JDs5S61HKR0FiJrbkOphTx73vB7Q2l0ivIe15P4eEKk8m5bwTOcqEGkTiC4g+eOAGH1ES/uPpYu2Rhirf1d6Ma9xBoQb+lT+XQMi1EBkdSqZAxEWQNwxPc1IKfcDoUO6ZFzcAwSpd/jzpmHMPwtEuA2EJGyErSA3Sja9jCScBLxD6uUsD7Cz8kAWbkNv3Aaq/SqcENGpCKpyAU15+JqYm0k3Y00ol2RKtQgXQvVCoF9xcloWuk/fnYU/UoCQqMQXZQT3A2muq+hv1FMT5fb8eZQIBEOjZqJgXE4j6AQav0aIp7M7tCCME5ytCSe/L1gaUJ6c5zUCadOzPJNs7AlKAusj4TI1DcJtuBi3AY/R06yWB6RPwaQQzkIqd8SK6kXNJRk4Uc/sjHKbgs2htQXN+5ZELEVHeWClvpSoykP7ms6UfDpXXTlijb43vVAgAqdXax+WvqiSaBJUI+ynkVNtjjwo4wRcETsbiWeqkbkboTxrr3cEsSHAl0iSgcW7OBRmhABB/AxKqQfHbQAQzG8Inu9RbGRkjeiuS9WLmkvyJk/nKxDdVVkbWfK+3RWMCNXq6DTxn+nyyFVUYGSkkuBXIGgEqjui6o1xUZTtYZ4pSpaIpjty1YtLFTzyNArKOFGKAvopYyF7WKJG7whdI2iBAl/CA0HqF8qMICzljggKROFdBHRxOnPwfA8BolcgVlQ4ZlySORBy9QcDIicwIlSrI4wAQg09fFRgZMyBAMdjCYSeh2F/Ez2SOTfDgBCtj7QEIg/KOAmqYiKaJA4IPzWdRqVNqe6GNqTsCSKWgRkBggGmHOoykSQL7wJAkD5F9nwPDCPPi2LFtFDhmHFJLpeoUwr0QnqsMslK93afJZnYohBzKF4flUxNXAsmCAFi5nZS96ZnfJn30S+YKDxpl6mp6pNKqYC3pDxtUTHRfmNqotLKe4AnSGLjl/PQFeWKJJwEY0+AGRGYO3cW5kXHehPGbYDCh9yEhe8B8gificX6slDhmHFJaIkGF0L0QlhBoxPdDJ6aeAEFWyKJModFFQcPBFrCQIB7MTBvj6L3bid1b07MEKT+oC3vw0KcjFSIpjw1XRAN+irjWhgt4IoEY4dcsC8vybb3fuv1p+59G611NlhbY8HwfmzLd8b3mDLIu9xOHovbvqCmbfWRB7qIzfMXHgv+Z5kX+c5WvtpPHNbvT+OetB/9fssuNyAaEA2IBkQDogHRgGhANCAaEA2I/xoQv/MV869iJf13gYh37Pnp7cyavmd5zq0+lmedEZHloUqvY9OskDvHOUyMhe1QVam/vEB5vhuNNtFVddbsEt2p4rh50adct5/UxhgOK2taGlrHkFT/uBqNIVQ/1Z3nFIi0Xlsc6Nfs8/j3tfzGtKniIRkIfbpyo0K1ayH7jtHzI/aAUBONIE+mRnRpJX5Wuw+S6JFzYwWEiTpYrXHDgiM5BQ0Dgi+EVAMRnQRFFCD00d56NLy/SlqjxoS5/AMHnzG60X533gFCZdjDOlvCSsI1Jc54DvtA2ME9IIr7SPYyJOtAhHcCcQrqsOKZHbU4Sij59flozMRZhtnktAGES32FnTsCbI0aCHVJQepEOQmdPOBKk1LKc35EzqyPIbLup+LCAmcHpG1yjsVpRXkMfmCZOzGlq5QPQSwRrVOBgJtLFzS/Hh2D4yQMCfV8QR3C3rhQhoLbH+Ri4NquUN4YhZ2BSKec3UnqSUG1OOicjkcG53DPhmDsk4voezgdDhnbbEBcxcVFYsqmICL8DDA2EFASl5TM5j29MCZUZJ/F+rPnR/QizsGPi/fqbdLLs/b6qPx5NCBC4TF4INi2JpfrUewFNKEodwSnbelSUCB6SUtqi3jNzAiehjgRybFJ8kfcXqBDoFlxcCmUFDG1eUG/wGHBpNIFuGXgHGgcUBtoaYoywfxAdTjK2JLhiDzt/cbGB+cRiVL0lUca7iaYFLJQr2jvm+havMFLRYDo1Kwj55rnIKgLU4PZFXgvd0CY3RGFBVFNNTT05Ekh9hrIBKPOckcwr4TKKRDCqZAWldshadprBLfjClWDWFyzlJeCQVWdEIl00AuK+SIJahvUfg4GhE5L3RUtZdH3AJsjSRSsxJFpfHH5vQnYwuJgtkqm+tV5h/VjChBwSQmjsBdC0XwZAQQsZoznoEDk2oMlFfetamoKg5WaA6GkF7Up0zoVCNAZ4O+mQGgMvTCFoNqhPIqk8fQeCIk82NUNIPijQi70AKFO5LIeuTtCVjpPJeA7IhQdjuTWiKEGglkcPS46LLSsW5OxssstSjwHnZouF69wgUxw5No9z0GAKFNTLtoYNjVVQDiuQzKposzzaDYyik5NcnwQtQ/xbdG6lc9xEu4c8Q5VIUPoOH3uifsXg3qgqP7Im05eal+V7FOTAprK5x03NfEaxaIs4JEQx2OKIDlLLBlbjE2OhSCkvYftiIwD2BE0i7EjyShTE7ukdHJ14FM18SmQCSblo1jxHLAY2mLNjSToWii9MbHmBKtOZHNq4X+ZP0EyVtPxK7mSYD+cT3gZRIkBah/gIIJxEoXBAbUL3NOZS4FBcubllhgYIRFDQz1QlG8yJPQLXIpz4XZkt1gn/a5z7dhgDeecA76ldBe0RH8ys0OKDgePLa0RzPcQdov51ITkFUQe3D7jm3XcIPHeKhEXH7R1UlvbHuN13LflG+9vbD+jJPKVf+LIAff0+nA8yut4XwQNiH957A2I1pkWewOidabF3oBonWmxNyAaEC32zeB+jwH+F0d5DxBx591Hb/tt/SeAwMPZlljX3yBD1l/ehREh78iqIJbU+3zLK/uWehXQIq7OWLaVKRLyqEjCIaF3ngAgZ+7obeRlFGkuS6Gx5NUa5j2Z9wW/nxJXY0gapWTH9ZHcnV+icq1BofkDBcI/E117qvCejIDW7pC5bEN0fwcnKpFSnJ2x0tYwIOWEJ90GzlenR4CoRRviBhAaiwfCx+/dUJa/BSNjwbrhqUTpLmtyb8ilrlSNwgyIkwGBlJHkjj0QcdY0sSLWfqBe189YASJsA1FqHSRpxUAQhwSP34bCxPggIMI2EPNKV4AICgQ9ao8oo49ymPdwEwh90DtI1kS5DMxWYFYEK05APUKer0fejBI04CLkfK2cU8BpuEIdA7wHVtU4ibYGHuQdkp1RtTWi05xD5mdswSEJqWZinIquxWlU9xbEWfu4nEZN0GgCyzumQB/B+bKIW0qJP0DnoxPtDSSvVH8j82tOzZYoU7Yoo2l4iKvKeTTFk6mWCoiMK80lIS/qugDuxJQZ6+2OkHTl4JQu8lvlnAJOwxs7iUiumDUEelESgLzK4M6o2kp69VGWa8rPDYVnWDEx+qKwwdoc2R4lr31coNzBQygpXe+YwrEYq0IMDiz+GIoKSVIgZM6QDHzPXI1Y2JC5Gk2NQl1VfFa7T7UYyICUvGhXCJcBqXZJ2VdPC8dBM8mqg0FAeL0KcBqQMO+uBoRKOqBjb8nO8G11YXlHBDA4HBNDpRiUv0FaMMr86IrcgzE9ovyLZdscU+SOKL4s4HBY/BQPl2FuSDQJCsdJyeVZbfBRLMpYoojF5kMVT7gWD0SSeVIYDtkWawdEb1NTHCyFCR0MviOCdymhM99ECmF5R9RAyDxb2hLBqmqNEDZJxcQwIEBjgM4FnztmV3dvBiCi++EdUwAEnZHF8SRX8UfV+Si0BmNwrAGRhGUrC9mgURgQWRVPAITToBD2JxLr/lMTNCd4jr/YpyZMTSoNVE1NyRxUChB9ASLRGpG08+lysTN8WzKY1aemUIYiOCcYVdgg/oZOTWJ746Kho0RiA6vDC5gYEPZenFqSAVF0PnpdLbVkqccBkYXcKQSsITlnlmSiRqlMTaZBIY6IBYjyPYKoi8yd8J9G4gA1J7ApROmC2R/epQT+JWfxQFEPkizskSRuKXaGb0u4Dfw9IvO1CQ6JaO4VJgadmVSngxUzmLdRFmutm44GZmowqyOYokansThWhTi1lPjPovNBZ59FjUT4JoWTYkCAj+KAyLE4s8BVJY6meMKL9YMaFCtfcx7/rp3D+7ZbUebdNnL4eQ+W+TeH90S5e9LPAPGOTvwCINaPfpwOxnZNvwaIf3T7j//o14BoQHxEFxsQDYgGRAOiAdGAaEA0IBoQLco2lL/59rsD8YsYLfnBMvRaHBQG3dfVx9O8i0sWxq3frzzXYxEDsnjjz/QwL44hyjiuUBSGqpeDO5JC2LC64NJgvZyvd4e5TPin8spLG2h52Z/9GS4pnkIVm7BFwhKIbTZMSrWQRFoAkVLYJPYE5yeSVoBIK4YbiDK5KHdYJh6IQV/FhXgEWC9hVdvjDiAWg7IBhP0oPAMiz4E4GRDZX0E7QISbQIQQbt1Ya0DEap8HYh7lDstkFYiligdYL2u3JNfkHExGKNGfnTtJNyrngLK5xoiQFA94CYMCgXOU9aEeLcXnhEuDLXIaPQujVrGoyyu/AlwPfVxYPU1OwsZgeXR5jHipchG4f/WZojYyeA0OjIJFmYNFuc0y8SySaDEFP46Sm4R+ibjNnEb1WQHc5mDiHvE+j6loqoBzkCELVGlMCP+hAAHVC2V9BHGkUJ8TJB7BFumT8RsWKhZV+cKvYK5HkGyvepr0KtUwanq+PG7v+BjQ96jPhNpIDF6Dg9UX+CxEeXJckW2WiWeRRFbmwKu0HBNKLY9wZ+l5bOCzwplPczBRZxIoT0CKgRbVXIDorjONiWAuLHoOuBod63l0ItgAtgJqBlvEszCWKha+fBc8xaArOhfUnnqSIELV1FioXDAQXfBndqI2IpOtaHCQOwyd1QmnpXdRbrNMPHkhhu5adBpE4UPHBGQLyECglPqsYJIrDibCXOiT9x8xFs5QhByUjSMh2BqRSlqf2AlEOVQmBN7TK7BFKhbGUsXClQ+jByIbIyiYmocBETAR1SoXDAT4EoUrMoraSKg0OEj0hO74EVFmF+U2y8QDEQYnmKEKH1qPAyLLHQGfFZuaKr+T5NxJqArSlxAg+pnGRCpsPwNJuBpJ1DiG4nOCYIUt4lkYcxWLqnyuuB5oufI06R0QZW+lcqFTU30mq40w7aFocOAcWieXUW6zTDyLhFRGlA3mxrHHp6ZU9DigG2I+K6FyMBnhdxJHcyehhSnzvBYDNCRMY0L5D2BxoFEwHMDEACNiKD4n0LQAW6RiYcxULOryhV8hVmrgP6inCTxJcJ5qaixVLqaJ6QK+hHNDgdpI8BocKJ9Xo9xmmXgWCamMICaNxsYErBd1m6H7zHxW1j7AbkoObm+PfL/c+s6aw4duNSk/731cX9v3K8Qi8sZ4lzvkc4fonwACflf3D9GvBOLT3FXeC8TX2tqPfi3K1sXfL8oGRAOiRdmAaEC0KBsQDYgW5b8w1H/51d2A+OBtyZCwbe33qBzASVhlY2w+OrXUv6iP1Od1q61ubVo6Vu+sA+RrnWd9rs8i3+uyn15Lcfcos7bFJY0zkt815nltcFNa++3UtDOMxaF0AOQfwHOYu5PkVUmHNdmFWp+jBiIuLojsIpgn5eNMJQOvkwNCf/WPrs/RwUhJTOyX1+MSCE1/sZJ9Wuvjvc+0JQeE89dJOz9iL7UzjLcBnsPcnSQvBm8biPABQFgNNRChBmJ25Xo6zCnoQ7rmfyIaGit3RK3v/1NA5BUg1NsEKQ7mHSC9MqhyhKpGsL8KnyHaHU4T4wodDGEm7OtfgMNArAlMDafR2CSnUfkRp8qdZK5e0Umc7LsyaqImimIG2YZQOghAnFhcXWNLoSupMKEvJE0v0etUgJAHfaVfU846aQLMx4U+nkeVqzd2hzm9gIWiXi+dsFvOwlaZSoIhoQ9e89PomtZOEPPohZEBrZtedChEu2OwyYG4CeSSAGbCnv5FoGQj67gov6FPyiYJqS+2SH0ydxKfL6fMVq+mCBJPkiuYxUjAocgwFKA1gkoPjjURi91BZO1/pE+h5M/Eg1Gvf0mscr9wFE9Kp0rjI4vpimqByDSm7i48poPzesmiTpJEraRPkg9GR6ald2BOBzMUJvwHFSzorkjsv0mOWtkYOj/jWHc9B2Um7OlfqCvKdJ2pPkdSNglKC18inSvWhqlXQDGD47yCT4G94FEUIPis6VNTgu+DxYY+dLrKDI6CMMDrQe6IAM7HWbQ3UDI5EgP6SCOHS4G0QDR63aPWKOb10hU2DLgruQYiU0NKQRxz0ahJokthQCgbIzrtjjfxDZGU/J7+hbiiEGVBgShsEudP1SfnThK8ekWga5zjFPKC7NV4FYgc5kAgNvRBXYKUD2WDW9YIfN4acUfIkFVAZMQfAETPhIQcPBA5GRDF62XM8hrclTkQmJpCdiImsTAykvir2NSUZYlEev2NaV3KTNjVv+gLEH01NfEeo5mcRudOMlOvOI0CWR+LuoUoXfQJtDaemiI+NSVigZTYgp+amCCTy8AFcHBiPTVlYrNoyQKEY5NgCFkLRKIvQPQA4uK8XohUgdfKVhGGhJAWlY8w2j4oRygLgm/3ETZoxHNIlYgKMRjORYVjR//ignZjUM7HaSxskot8OAgQoyruJGOtXpGDEDMv4pIii/WZtT7OYJGQKsfA3yMSBLtMdeM8qqgWpuTCBbniewSMzcpiTf3SoyHN4oLnTJTedhJ9J8oz6C1YKOr1Mi3d8hpslZ8Qptj7vuqrzeF9+hc57H9dynfWM1cZuTeW+K4Wtbf5jqjv7cFPAOGZCTm8n6nwEUDMI30/a+JeIPZa+MVAfK2t/ejXIm3d+z0jbUA0IFqkDYgGRIu0AdGAaJH+Y4HWte618XHt/+bXfgPiywBRsRz2dRTmW83eSNWgHKY/vB1liH78RX8/S4lnfvX8F0p+52N6BranVz1/tlWljuVvB8TylOO8RivytOjWcXO0nsL3F339/bU+UmI7LM4q7T7Njrq4wHKI1dDWDijYndcQWAEiKRBHNEl4/PhBDT5V3X7iV9+PAOKI/Qc/KIfDNhCH0utVIF605brDvsZjieZpgfJxAZC29xSORz12PNRHtoGwdudAPL2U4zOWw8YdkZzuxhKBVSBe0B8K8QYQR+nd/GoJHw5EuH1HHNZa9ECUSh8AImwBgZngEBQIZimchGlxFRaHsh6Me4B0yWnU1FEYruKkMtfU+PFjavLl+fV4nG7glx8/qMGn8Pz6/EoB498CxEt4OU4T1R+vz3/i70B/vx747+fXP15xxl/TH3pHx7/zEZocUOZ4fH798eP5zz+4xBPv/f76nd/98Uqtz2uk/dProKWf/6Q6tE49pvWjpu+vdEfoseOhPkIRUmxTLzhabce3+zQ7+vSC47pGcE62lwfIze8BrAfPPRB2gjyMHpGpnnLVpB7vNTWme4CvniNdNke9I6b3B7mpDwDi+UiQ0cz7FL4djt/47yPdrMfD91f6+3j4RlVM//5v+hMOVIquIdpDHfjGZY4vx8OPH1NrXJ5qofudSh34/GWN3w4HjoJK895vx6PVqcdwHu890E6KVI99f62PUIQU2/Q/R4t26naf5kePOI4bOgn7IsGJ5I21KbrCyDDuAaahxEwJMCoYiHAOc00NGt6DAlHuiALEtEgDiCcCiUqWo0cAMd20B/77maeAYwAQz39RqSBHeBrhLn6f7igGYtqPWp7l/EOZQmY1ToNHUVBpnDXdvaVOPWYtoT66I/TYH/Mj4X8H1Pz81/e//newSK3dp9lRPS5AlDsiKBBhDKMm8Y17oECoTkYWIPIw19SgCecoQNgaYUAcDwbEkd47IJ5f+IoPACi8eiCOpXt0xIA4vgSaml5o/3SlvXzjaxW1HPkz2aLGw0EGhi+ZwHdEqVOPWUuo78kdm+7C+kgBYvoT/ucitXafZkf1eBCWA7MUmBlRTU1gPRj3QCWAjIsgU1Oea2rQEvwCIA6HJRA8CSkQL7yuP8neI6+r2j0deJ2apuHC1IQJ41CAOPLUxGdMc+808x5pfjlg2MJKjd/oyLcCxIHWCK3TjpVJcGqVZjvcERLncXakTE0UpQPCtftUHX0qcQVlOZzZswSL9ZCU3GhMDHAPsFgnfk8lBvZdUV8Rr6lBH0pfaIn98+lVv0dgsf6OpffP7/zKgKAZl/bSEkjh0RJ24DOoO/Tvy+GFhutAC+ITL370+RtLJv093RGvdMYE5HRtU8tPvBAqEHWNf1BZbu+J94YDltGnV38syBlTTX9OV/0rrxFyDC3bEYpQFus/+XU539p9qo7i3ufF2lgOeV2ZouIk3FbdyPaFLlRf7za35bHjIbxrm+6Iw/LD6UbZv8LWZ8zq2Eq5h2reP/9pbSDWgcAdcf/2EUA8v4Z3bXRH3AvEyvZO+D9sa781fZGtAdGAaEA0IBoQDYgGRAOiAdGAaEA0IP5bmx/8NSD2wVkevQfM9wD+r79I/mVAHCWnq+9C9T7432G2f5GRI3bm86LI88q+R7bjrK4D2CJybKWzabEv7R9d1oGn+epBFSsC1cHnUgtXl1Sfk3J9lmgPTDmEv6+rQBwqIA7un8MGEHUZq+lpgdzTgrpSV/dkSfjV40d39In/MPUgKFfEpLZTGdhUBLjTChBizL7YUzvE5Gpk8ZxqDUQsCh1LIBKfE4f6LKTZ4hCHGO4H4rgGxFNYK/MzQNx6f5zV9QS2SFCuyAKIvAtEXgCRHwIiQ4tEgAj7QJyv9VmRKyWjBAbCWA367ruwHpgv8Qq2BPEx/kBKx3EQnl/Wy/DEwTwNx3L4a8qZHRWI51f8+f4XtUbsCd775zOxM4JyMRbH6VWo6nqh3LhxRVgnoGKZ0D7dy48SX9RN5VSO2lm2R/dB0/5UVEDgtxLUHYWv5hNPOXhAekiqvYGHmfU8uiMCdPPlLECNO2KKxVgNtIH7gHecNDwcSiaZ0odTXspxEJ5Wy8i1q0SAA5gUzLYoQBCLQxOJB0458t7jk6TnvyEFOT8+ZYnBrCh1vRD1wLgi8uC6Y5noHYFs+pRz7AOrEMDtxe4I1SCp7oji8qAP5OMh9wkIlnOJorzQ82Rz5ozmIG4paqiQxX+GY8v06L+elcsawdOasRrQ1efCejgKP0GYCsiszjgI62WwlH7/64m4EZqT5eEqQExnPQek9KfjR9CsDAgAOD8+nSEEr+PB3xHGFWFljIplUu4Ip41xvr7JwLo7Iijpwd0R5nuSoJ8BZRvM91EMPVCn3BHFLeUsyiPkpwIggjjJlLPc5JeCYzXgjsAVTB3XIeTj0//fDoUzETwQyzK8vTIdoJQAXcWAmM6eSoBochSe3/MLgHh+kTtpdnw6g+8IqwtrhHFF+NquWCZljRj7oo2RlfLg7gj2cAm5uiOK70kPtRDRgZqmJpHJEG0pwAU1EHVLAR+G3V2SAgEnGT3LHOkUCBlUSmSD+6ATAbEVMHW9MDeCOROOg0ADtlaGanJT06G+I+ToN5maDjzQnEYn6JgNUaa02XGu4Y9XqwufmowropoaxjKxqQnOMMw1SW/V0XKW28P7xPeEpC2gAgLDG+ZGTq8HcTuD7Atgvlxwlk1N0AYhIKhKO0tExwyIwpPAlEGsiwOWxgO/Y/4EcyOeXmuGwvNWGVmOvwvT4lm4QmBuyFFe9ImNccAHAN77fSr9XTgZzKyYHSdO3NHVhe8RNVekZpno94JOnGGg0DEkuL3YtwZokAzVdxH1PaGJXFxeBnGFYbcVGlKZmi5nVTPJOMst1hTLQPVOZw121rRiDA6I+qPhXsr9ePee/cT9cXOvrVR7Z9Xn3//NOoe9o+t78kxLJG62szzLb3H1nPjoIN0+9kiZ7e35tf73njZ+BRBzdY3HgWi/NbUf/RoQP39OA+ITBrUB0YBoQPzsOQ2ITxjUBkQDogHxs+c0ID5hUH8LIL5qkL/dVRx/8sz/KhC745bda9ULpt8al2qm8jt+pgTjebi3ztnGZ9KvmeJ+DhnsaS8coGkv6xUP9Os9JNutxrxRe/fggHQrtQKIM0vAl3S/+smku/vHZfPWmAw7qGS3N5ef5tXqQmrmv2mYqDKo+W8hnGZ11nUgGMoBs2eDdDhlSP9T0514ryM9pqYd60DE6t+VdP5SwsLtrWuFWUlI3QwIAabqRXZDl1aBiLMoTNlk3Zxkhm6Bofo9UU9FOlGAcD8Uxw0gwh4QuWJFOPHzkyjZ4/dotUlYB2I+3Gn/Qt0FQqamfAoWV/GTWQcibAOx3LkAopNUCRxVgipyQBt+BoRyGcCIwB0hmamsZ3VDN4DDkLMxH6zOuo4CxOBZER6IqEAMXHagZIo5mZzg4iLRdyzx3okjSxfOxQ/lpH4txT2mHEtavq6VoqW4GHqJi5keLCSvrBB/XuBIIDpfczlOGBnO27GYPZ/Dk6KNBNlp5AzZ/8AOB4lvxJ5V8YmXMOWoBqwRSbkMiQvxGgEgzoOeFZlqxSYFWZkPVue8DgUijrBDiCUHLG0nDBJp0ZMCvhBRipMJ1zaoaQH1BYwKGBKoLwTthV+LuceUYwVsXyuipbg0KrVXYMODXlkh/ryAcQxLLkev/ikJQEQ+B3NOGQkAgWWwuwbxT5HrMJwC7giYpigzAk4gwc3ENKXrWSjHBjHif+LrXNQxqNWG8RvobDVhKncELpRBbmxxMuFXsoh36AszKjqJjO0DxFuFrD1iYXMIEDDhCPBrsVoRLcXFF5byLoK6Z6hBh9p68CSd6Y5b43Koo0wqAhtqVOBGYgpt6IYcdKY3Iw9ueKiByGA7JOcWNehQ52RA5MB3hDid+KOLOhwQnhWh6h41EGU2FycTfiXRk3ZIp7TGUS4RmTLgzZIGdoqpgegz3FVmtXogSlybQPCMTx0e17gcSslJQQU2imOEjQSaEc4P30YpwIqGG87+U5OYLcEjJFVAZD0rypRAa4ROA77ORR0DJdbhdOVYEYwiyFf41KQd4qlJ1hBMDpgQghJckraEKRK1wZslDeYeo8e6S7lEfK09gECLLi43NWWZavS8UKb4OZcDJDXqMa0Y5Owi3lDBjQS+KYijCi8siX1O9Mb13yNSEC7DJRn3OeNzvp5F9xeVmz74XdT/xNc5r2Naf5hdHQfPijgPsFaavlFgQRywtCZ2OlEnE65XXGB04QOjAotolAWZeBXwazGnGD02xSmLblUrR0txnR1bA5olZ+FnsNOMOw92a+L3UnE5TlA1ucDCjpxd4ihTkx9N/x3h57fy8bXU6Ujn1Wv7wPzIN9j9b/FbPckPnyEfXx/Ycp63mR9qLax9a/6A7QFwP+6nhK2e5IfPeDwuZSntAfEpI/1xW/vRr3X4t4yrAdGAaHE1IBoQLa7W4S8eVwOiAdHi+j0DLBF+xUjfEVP8uKoaEO8avfITN/0lP9i6o/oMmSXVw0O/LS1aymf+hZd/dx2J16FPXKJVY2WY3oXFM+ds5AoIyqxzrJJTuW8jifo8a2f7HaK6zR05D1SSHpd0Ud0aHovZA5EEiFCrSpRDUV5Y/u4GRjiE9JOmLnOY+YTEsiOVtH2ecTaswhqIkCTWBRBpccfb4SjJovWh92SDJFHF3b4K9SCrKoeLand8KiD6NSAq1kVdW1pU/SAQjvixAKIa7ruAyBLr4ufQtKjhXiDCHIj7LjrJ5PFj+RaVnXOS1BCzCa7MeLhosoSPS7pE2RfdhdgNIc3YDRflPrB5Pdd2FfbCuVjWg20B3gXO4tRKWAAxBK2XWRmUTNHUFDgYJ3Z94dSKKF7I7T+KWoYCMQhDJCDRBm7FFVFJbZL0kTbAA4nSY3jFrHFczqL5IcmuoFwQSUZxG+C38FQ3GhBx4KhIlyNIJON0IqftxPshMA+C9CpKVjXkCxKImqXNPbEbwDEydkPue/Fg0YxwSm/KXghKj0mSuoS6BZ2FpCatDKzYL//LWiAaGBGp2BSYIZGEORFLPjgIfBiUHmoZAkTkPF0MoehmJNHiSAEqGziPWG1MAxiTJER7jMoQpFTNcUFLmCvpSWzNZRs75A19pkxoyXOfZI3gqC4Uz1sqY828imung8WZZmhZCBC9Vt0JP4OT6kPNujBiitaZxIdCdTGEPMKsii6A4QCXlkhsEe2ABhXEoyUoEMq0UKCjSFpN/167slSCU7G2RqRgRAYDAhcCt5WNDIBhnfaxVwxKzTkuSfrGdwRTM7vg2SFv4IRktNAFBZRjS7q+voHdoXMdKU0I/SOXa99mNgFC9CoYiDRjN+AaNyBySMUQBMwNBYJYFaSMIZSEkdeIIU9T4BwIHC93RJgDwXXiahvDaEDk1cUaS0S/AUROSyC8V8yS45KMiKC0hrEGIge7I9D/vAJEGWtMN4VdR4yHHloWJcQye4M6ps51nt1ArAUBQpgNBYg+FeaeyJJkuklxFlYEIm3kiwCRg1jqYLoTIAqPRDkYNjWJaIl8rso8NWHahMQVgEC0p7EAkaLL1xP9x9rA1MT15jICM45LEp0NPqMvU1OaT02nsUxNOasqBx3LAoSMNZaai/miZCZhCsuBrwcsNMK+4MWamB01uwFMiTNzG4K4qgwJ7IVYFmuwLai1LGfxIssEMKFABvoeQYtfEAIlL2YSTwrKwchJl8WzRK+f6SnSKcKhG+AWI0p6AgSLCDElMgrrQ745XHwbZ9i9XWRMZGRqPkqUvvEZl3PhguhiPfC40bjoEs6MSVbl4Kj4g86QtN3lh6zF59/5tv7dMIdHtrz1UfZdtS0j1Dvi56L8iK1uMd7/zfp2qJ8ExAdu2z9xfG679/T0A392ab81fZHRa0A0IBoQn1RVA+KLVNWA+CJVNSC+SFUNiC9SVQPii1TVgPgNKv6plh+J6ivD1oD41VsO+7+/7BzDD5dX5GCjJE0zlJd9Ocn453KcpSPw6O1opZa/QFo5On7GcftpUFte/WkSxyqmRLoNRGFXJOSRENOWVgfHKFHjqXBkGPmsDVUHdQjfAWLZnRs6GgMSkJx8GZOKN3C+uQbC0uo4Ls4JoaSK+HBOC9KBlIvluNVmLXdVdNEGV5JKdeBpvWw5Jwflp4Sk19qWVoe7yoLPUeYlf6Lob0huYznae3fEDR0NAyJH4STsAWHHrXs5bAGhiZbS5QJE8EBMLZ827giLaguI5R1BWSnlp9idvaXV4evLSbX1e1w/K6IctL9PeV2vBvoYM62Mu3Q0IrgWlOgYkgGR1WNE/58SKgNTxgY9Tvk5OJVkPHB74TYTeBTgP/Bj+FKuwyO6SXgfVPvFWlZbDGlNOBc4hotAVTFUK2OlrAIxnEblp+CC2tPqsFSXCLhMf6b8OwNRObc4/Y0TA1Grc1ASifkIC62Mu3Q0AEQHJQ0DYoiCe19Sq5pR1ePFlwFPSHPqFGnJVJKMMVg5JFxp9u35cfaeE5jasgKhniiaoqdjSEeKZkdfHo1flFUg4pRLr/gpYU+rA8xEXP28RlzyJRLrJXBitTi3eP0N5cR4dQ7W7tBcstfKuE9Hw90RyYA4m4gDg8ZqFABi0OPn4lQSuHZqDRy/5PgPoZSTM9x5zAaUlhUI4o10hRAgxwZdiVC/SF0sy9q6UvgpKdzS6ii8lGnvCZn/njPo19q5xcs+KCfGq3OQdscaN+FeHQ23WNMQ6R0hU1DhguAIvKVw3JxKwOkhLZAaCJmN7Y4QcRYFgvUHpGWRhYAix+iBoOGWO6ICYqWsA0L5KVg997Q6jA4RoFriFmvv3FIBkZbqHKTdEWr1iYd0NAoQmFF0jZgBAacp+9SUg839F9SWQ5maUj01oRymJgGrjzIg2nIBogwQAYVj+HQC5oZMTWmtbMX5EH7KEG5pdQSnYSKqJcJbqZ1bvP6G1uHVOUi7I2DpMCWNB3Q0yveIbnTfIxLWAtXI6Li0+x5Bw5PUqQS1dWXBFh+UpIs1ymFBV95H+aQuLZ+Fx4fWRI8jyLGrfjbjFkQrY6Ws43woP2UIt7Q6ymJ90R7L94hL7dzi9Te0Dq/OAWLn7NNu/RHxlo6G//B3u6T/0JyDKVOsKVSUxXL9/LTestXvj+Xd6PIO5+OGesYHbPv1P9T4e34YqJ1KthUqtoDAGff+xJHv7MFafZ+tnvGB9bffmr7I1oBoQDQgGhANiAZEA6IB0YBoQDQgGhBfZ6D/PTF+SHyf18n4YaX+XUCU3wbnlUA7gn/JTCuKFvP3RVnjTErGV/0FkvZOf/iY/NQ1bNZR2BFZRK+3txyUfxGyS/uXY9tt5LuG5b5S3W7Z7THstoDILsWuDkCkHZFBAlgFIta6FibogPSDuUFogqfSXl7P6SenxrHOL/E2GuBfhPEeIEx4Id91H+bZ+1qOwiuCbFt66BjmMB/DDReavgJCK6FrLcMaPq/8IDqnwdTKGmHsRdPAgAh3ABE8EPE2EJQEynP+xRoQ4S4g9u6INTuaW0BgDHOYj2HpmepMgBkBjoFqboAvAe2IE0w0Bq9o0ameRlBdixVljSk7RfyGdSDUV6XmPJz44eDTqO4n8FHBO2WSlEdmVXtjipCzhFWE5rZyUm+TsRMRbmVlmG7GuvfKnh+Md3HxPA+NUb1VdAxPo0WI3kRJzbHeRYDqRa8cAzA1KC1PfAloR0zJvdHYDcKLSMqrUF2LubIG6d5zupCfQz6zmxY9bVzkJVKvyUbHeUhJNTFEBaMY3ohLiXOAUO0NijDnRYSDesWgf0n8VjwrgygMb2nbe8V7zMz9YLyLi+d5CHGieKvoGPbONyYULksQDkWC6oVxDMDUkGCylyxITtHifM3FGkblFObKGlTmTQxeeC//Xchixd+k5jxAyQOcEdOq0daoTg+EaG8kXDqp0twobitXrRl+K56VMV3T1zd5tea9sucH411cPM8DuiXmrbI2ht3VU3VCmb9dahtMjW4AX8JXkr2iRVgCsVDWSG6xHjI8WhwQpMkhpALHeYCSBzgjKkRhQOQwByIL/4I+kxT+ReW2kq1m1vDIwTmhjIU3suq9susH411cSo296JaE4q2yNoaOy8JA9Oyv1Qv3oMj5CI1EBEZ0wIdK0UL+0NQkN/xMWSNXQPBixX8nEGIgnJPSnPOgHdUaY8WVKnZNzs8E/IuQLcKZ24r0T/kXnpVBR952vFf2/GC8i4vVCJ0R762yNobwXIGHV1KtCCY0XvR7hHIrzqJrIdoRo6tEGB3Kq1Bdi1pZg1eK8j0CkwqOYRIDOzyOc84DFDfQgryXxZohFH6J9zMB/8IDMXNbkf6Bf9FVrAz6WDKkbe+VPT8Y7+LivFWCxqjeKmtjCM8V4rKoGM/WxzaR6lnlNtz+1Jfu+1B430fIW9+uP+6bdX78jJtfFPOmaEb25281Lc8GjO8E4tM6/tWAWP4W8RgQd/M42o9+XyS+BkQDogHxO3WyAdGAaEA0IBoQDYgGRAPiPzGGv2m4nxxr/AfObEDoVn50mn71GEiHeHvL24fkzMhZOHhRy4PiAyVLkKDZqyMLFyOOGySF+dmrx/fYFPlG+77m8/j3dU4fyXIsB+WNbDJbZtvf1xxujGt0RjZ4Bnnr+r6hzCF5NDZnT3AS6ITLgTxOrDq8NM9IwsVIYa9znnUwjyOGdTuN9AAQSmXgzEFaOb8AscVsMbMRiy+Gm0CEORCO6BE3gAg7QPhc30kSpvi1twZiObBRtTDCBltkfhGktXpiWF5I60DEm0Dos54bQGwwW9JKeHcA0QWVOI/yVL5yFuiB1/uVOdaA0GeRoVCgdLJay+KsTA3R1jiNOXguhjBFpqSRlioeMGCZjI6TEYos0DDzThmtZWV2qI+KKmJ4zxgAkQaMx3k0FxhwQThLM2O2aKRnsECcY0sQfxZzr7FW8WoqZaL/AOI8KGeB7oj7lTk8EOcRDI4ociGUv5vyVQO8S2oti2SuJhlcjJNjixQmRxpKKUmSqipHSa8mniZAa6FS3jsF93nN7GB5lKS6HLVnDB1VaQlLuvZCgzDeCAERXSqXOSAUX/aOLQEmCGXMfKt41aMw/E70jjDOwiPKHAoElT+F2R2BXL/cEb6OLiTj/Q2gAPSO6aBMju76loymCQ8Y1H8WNxWog4BNEUT3wyw7QN+pmR3iBcO6HHPPGCgqFbEVACEuMCgpuh1hptvBNaQAsQnzp4jC7yjcF2tVX4mgwmhrhHEWHlLm8FPTsAZEWSN8C2NyV435n+i0izb6YothjLoitxDgppKk47ZGeO+U7FpWZodoEvC7QpwQz5gtIApxrjcgnG6H1LAOhPaHylmr8iq5qUk/NZkux0PKHMLH45AzPjWZj0gIJmE107JIWWb/5PxPNMGONsjj5XIxMmgyJRibmjAVON0P750C1kjN7BBoWJcDq4J5xsyAkMkkl6mJ+I9Z7HFMt0NrSHJWDEq0MVKblCmt4lVPbLTiR5Lxqd90OR5S5iC/EA7nzApl9D3CfERUtSKEuZYFcTH004z5nxgQvLQFXAz6yUWVNcAyUU4G+gLlP1yh5p0Ch5aa2RGCmqelwoRXzxgDgkVRRCuES/BiTV58YHl43Q6UiNw/c2yBkg74HalwObRVvJIPIutbfFiZY/8j2u6HN962vq3mn6r1njoeVdxY57ZsjeNHtfrJsiC/Boj97VFFjHVuy6PbZ+t8vHtrP/q1zv2rY21ANCBarA2IBkSLtQHRgGixNiB+l1h/9yvm8+L/xRIsDYiHgVDVC95KCjq7nfS6e0DLogtr7iSuhWQtI61j7iX8I1nm3y6v2kI3O4/4ImCJLLa138XWuBg3fxJkUd5h1a0l7YxfCFd9htyrbPDD7/RvqP1b1Gsb9aZtIGIJMzqtBi++sOZQEsOaO8kSiJTML8Vq6uTB2hwsilidx1k6USOfhb4JRCwHt4BIMyCmXFvuwu02qvFzNAF/ZnlIeZj5t5QHg8MqGWLljrgHiFADMXMnWQEirAFxClHUw6vLYQaEdmF7kJY/4yf4tNwBBGULiYNyCrfbqF1gbgERa/+WGoji+iFC5kh+1HwNPA8MloNXwTBuB/QrMhssKcshpzkjQ+tHmkTdH+RZY+kAKTFkcRyR5E3Q82ogwC9Z1/Rg4XXW4NCenIWZUqt0KKviLD2FnwtNTaLZf6ON5M7qyJQhnGQUVJ0kFF8Wejze+7cgz4ipixUjErt+FMZGXytQ9Oh0ryodpoIRCrdDTWtg6aEsh67S70CqEfWLC4nacPQeCHo2+TT2SVURoiZ0+yUQSt5ZanqoJ0vuEX+fcEfUXA5JcA7KUglimuCB2G+DFDpCsVrophWsL6Ogaj/nUbL0Y6z8W+jCd3cEgnhLytiYu6xkiCsUlY5UtCy6q1PmECDgVGJEAFPHoOVbmRDd1d8ROdVAsFSLpOph1aHnLe6IICoaK5oe6smiRANZI2qVDjgdyTvwRajG6o7YbYMfzb+qC0wYlKWBPDX2nrgumCjUtiEhrQCRi7vJ3DckB8+6SOYwYiIJKpuQ2amkXzIyvHCWDv06EFPwYC45PoWeNwcCuhrrmh7KrOhXgFAuB30wgEKIng9dDg/EfhsQq1AXmFhYGnJHoM0BIzFfrFeBIJ6EupvMuRZuaiLWhpGlODbwJRQIJpZdloyMXE1NooxRTU1lGsshGz/DT01yvHxqsmljqekh8CXSvBAgkkxNXqXDEb5MlwNrBBy9brXBQPQ6NRUgEhxhQB7QqSoMtX9LEDsq0T0Cq4EXJXE3qbkWGa4iF1XpcCoYF/oUDb5ELFZm58JyqPQ7UrVYF2UMLIC8bLKvCfH0aJlVJQ6qT7gmFylRvkcoA2VN0wOLNdOBAnRFoizWlUrHaOwLWNnwhxb6lD8SD+V2Gzw1XdQFJlzhtyQqHQH8ERnB8Rxm/i1DB4OgcfHR81FmwfKjIGp5zzfTGOKN5yy2S1AMNbPESuaVfdjqM+zdumjDXhtrta/ZLe77t7jtPcyCvFLLr/6JI7/z2Nb2MQokeafm9lvTPxx/A6IB0YBoQDQgGhANiAbEr+lIA+KLdKQB8UU60oD4hODeY0DzvuM/E/tH9PtDx64B8Yl1ZH2Rtg6UH7OupPnOztXwrt6p1RQ98EzbWsMJ4eUNrsQ8iIyyjvWw7GomB5d490B0835XY2C/uoHRsdpy2oy3GgV4quzWsgnEMAciwhdkVP5EDGFGznBiCTlICW8MUtt9SLZqXHIlQljR7RhQ1igDaTnUI0l5rCuF0ECIxnut25Fce6lyUklWMzE6bpEVZvGGWteExm63FgNidl2uAjFdb9keWd0BItXd0aOujQE/BVN9p3vuiAFl0Xa3CkTMEGq3u3UbiFABsYatB+J0J2sk1++crgmN3W4txRvlUlgPrHsRw1XYDWVqoqEYkgPCmA9ek4OJYUXRA+kgPar6HgOzOyg5gmtEdTFqPxPPJYkBZXHF58FUPHBe0OMw/RjmUamsD0sWOd0O43jI64A+82PN0jukTe9hjVi89M7rmtDYRdHVX6tFOA1B0pCmexGRlc1mG9Chq9k/xM0ZvrkmR/CKHlDl0KNRVCkABKcLCxBLPxPPJdGyuOLBuijeF/TMvdY14vg8qvqOMN0OTvaqMkifhBkCTR1NCAMIpgndYHT4eGOla4I1gnOaq7UIA6MThoTpXigQdAXZHSGeEAIEmA8LTY7gFT1Oi6PEbHgD36kCYu5nAm8SraeAJh8UTMWjk6EToGTqmkfl7wiv21GzVVi9IEPoKw4o24XqjthldPjaal0TGjt+tVGLcDKKMInpXrA3ELER/BqhBBW5I1SQodLk0AEUrYyhPgo2x1vSxRoZXgZ67mcSKi7JgLKQo8oXp+Ix6h3BhBtc55d5VLM7ouh21GwV2o8+Z2MyVlPTDdZIpWTi39VALGtRckmvU1PRvVAgUg0E2i5T05omR/CKHqrjgaPKrLhcCAhmveWUbWqq/UwqLomUzZVCBrvB8HlalwxvrqPCalSAcLoddLywVXoBQj3CEtRIwOioCW3rjA6LF/wPfQd+X2SG0HotuJlAtQTrQXUv8iA0Qf89giLX7xFn0eeYa3LIGiEckKJwwUd5CSP+A5OUOv3sj2Vz4WdScUmy/54ArQuoeMh5wX/PoNaqdqvvEV63g0uqMsglsU4TFlLmarMaCRgd3R2MDosXbJPyDrMJ1bLBPQnzD6k5bG8//w2z/oi85ErUrdeMifV0PsrkTbOMrZaWLay9jjstP8Lo2LbyWFc1+XQgbtX4aOvZ/ftrf+LIH1DHu2Vk2m9N/2wdn1NZA+KLVNaA+CKVNSC+SGUNiC9SWQPii1TWgPgilTUgvkhlDYh/y/Yru/av1qJpQGxueX23/iiVPqLWPX6H8Sbmx/4u+hZ4XBaaFvwI1OOKHK4/K8+uyWZHXFvh4bbcCOR7xtof3GJULJv0niHLeNYdVPb4HfoAblo8yRfL/9KBAdyKLjymyDHvT0rLqCUGO1LaukeZww48qv5RAlhz/aiugrTZt7B3u8wC2OB3oOQeEKECAmoZp/CYIse8P9uIuSOlrXuUOeqT71f/COschMLtYHUJ5XkgeQKmB3Q41DMEfI5bTAzcEev8DgSn71R8vXIkYd4FlC6Q3YuS8LytyFEeXK76Q0OhOho84olagdKIHbG27lHmeK/6R1jnIIhehahLKM9DTQjAtQjeM6Q81b/HxMAasc7vAIz6jjNm6qaCCUN4F1C6gKaFAXFLkUMetJ/1JzgdDQCRJDlaHSlt3aPM8V71j7DOQZBMttyayvNQBxBwLYgfoYPdibnZPhMDd8Q2vyMkfcfH2EtFjTCUdwGli1gW0Lu4FaKKMe/PXEfjDeSBoEwPHInVYv1p6h/rHITeARGLgwldQ9DqyELEUSCKNcY+EyPs8zuge8OSClAEGWsgRNkjpQELqAFxjyJHDvP+zHU0FAjM3+XIUAPxSeof6xwEqGuouoTyPGBwkdUUqVfPEFCz1E9lm4kBVYp1fgfO1ncQK0JLMMJQ3oUCofP2/Yocy/4Ep6NBrVwuSQyu/BGvn/F56h9hnYOQ1fuD1SWU5wHlPdWxIB2Os+plkD5HuMXE6MIOvyOo0keCyocsrziLJwzmXYAHBU0L0ui4T5GjWqytP8HpaMi0eR6jLNZ6xNq6R5njveofy49e1Uet2d7093VNq0OZFGsfVz1HIYdb32HuP/IRahnbsdV1PN5W3mhxW/3joc4LOfdd53789jFqGZ/T1q8agy+xtR/9vsjWgGhAtK0B0YBoWwOiAfGPtNWA+CJtNSD+6ba+svzHv4fAs1dWjjUgvgwQnLcaP9c7hP6YLcha7VvqH/xIbv449Q8fy0oh9xJl4476x15dU+8P0x/ejjIWP/6iv5+lxDO/ev6LSypD4R6liZ/xDjE58k0ghjkQov5B544fqP7B8u93qn9IQng9YN+vZVTT0SOAIDx+/DgcQ3jio09S9olffT8yEI+wIX7GO6TIkT+s/sFn5o9T/2AJ+DvVP6zsuvqH9WtN/SO8TFAwEE+3gDgWhkJIn+sdQn/wUPCj6h90pj7Z/RHqH4F5HPepfwTu5bb6h/VrTf1jGuaX59fj8ftrePnx4/tfNPTPr8+vNPz4twDxEpShUD+I/fHeIVO4I67oR9U/6NycP079g+rr033qH1T2NG6rf9BxBWKp/kF3wvFwPE7z01HviOn9gYZ/2nkAEM9HhsxTFz/TO0RFSB5X/8C9+nHqHxbLbfUPhWRL/SOkWC6apfrH0zQpCRDljihATIs0gHgikAyI9MneITUQj6h/hALsx6h/WCy31T/4Ikjb6h8GxJr6R3g5ChC2RhgQx4MBcQzGmPtk75AkHUrpUfUPvjs/UP3DYrmt/uHKrqp/YNC31D/C8QVAHA5LIA7HbwbESzCGwvVzvUOSfFOJ46PqH8x9yB+n/mGx3Fb/cGVX1T/oeAqb6h+HF1qU/3x61e8RWKynxZv3f+dXAsQ/6R3yX1L/eOdPHPnhDuedYdwfzs8F4laNj7ae3b/tt6YPBOJnyjYgGhANiAZEA6IB0YBoQDQgGhDvHrYGRAPiY4D4Vd36jKH8ShD/o6PYgPi4Hh3oF0D6V3fj18Hj1llTuadXZSCU7hz0BPrnUE4+4v/CZXh6fXb1lBbdGc91Y8en8P2vpxf8bvn0Mv2GHITzEIwbsbuluQQD/cZ6hqrz7JemLtT7su1PdWHjs3Qlld0VdsXf11zVWG96bp4DcUS3DprS1sTFNhCHgya+HRAvhgeG9UneeSAOpRUGVF4+OSCeKpQOx2c660gATT/rHwkYTrUH5UZsjX7cAAKPs2dOwoaQV35TzvI6eXIEjfDg6vfKHsleRXnlzqyvCXeutY1YjgqEXcU3gQj3ABEMiHK3HMrg+wb2gPiDc1kh/MEpFQFC2npaALFCfMn7QPgza97VLhDhJ4CwwCogXo4HSlY8//kc/pj+ViD+oH2vR4aJXv01/Xl+RYlpgI9PnG368QPlpuQHn/F9KvEdJKqpPnp3PP7xGl5eDs/8+sBTDL0jfsMz339/vT6VSZGYDUidPEtNVO/xoLmtwNlfKqPcCOlUFu2OAY/PqkrINA0JTwT8kCkjPuYr5deYqBMovUMcDn2aG+9InKBjvkkIosZx4aOSOGOQhPFCe8/XUmrA8J8kwRb58WnwToQZI4/4+rY5lmkOpo5y4u7blNZTIL4deC8oCNPr/01/wmHa+y3QGQWIw8HOpjnrm05DR5qaqMZv03REk/tU8uX7K531wulCTRn+z02K2D+VDZxf5DuEIvnOACkQB8eNALMP6VHhZqVcVEKMYQL1C0oEnpiDYVNTz249Uaam6d1AQETmm4SAxDA9HG+pZMpo4jU9eU3HSikBopeUM7U+JChuJOEBMBMx+bY5lsN003//6/kvGYICxNR52ov+BgBBpY6H+o44uLOf/zqEYwXEkaE8hP/x4H4PIJhMNfG0NQfimRbm6f8jn3nQqeqwvCOMG1HuCOZQ8AJ5zUHFKTqReyAdjnMQIPrpmk96RyDLHQddI0gzgwZWxVFkiJMAYROcMF5iXUrqAael41dvCXyYJJIPfEdUbXMs0yCDa4ChfDEgaO/hhe4ABeJ4mAHxja9ODNHLN7mSZWp6MSCOAsTxRYEIUxZX2vlmQNDeJ6pPgMDUVAOBNcK4EQoEczB42oBugpABxgzGSGD1CwYiX9IQ3B0xB4LuiDUgiA7hgQDrIwpZJxe+B/ZxuVGBYPaUAyKnGoip+EvgofpGk8uR18zCMpj2Hl541ZCpaSrFU5MC8fynTBPT2eFlmsu/HeQz2BMPM3/MoUrCn38yEEcAQe9ehE7y/CeA4EvApqaD3RG0RPt/6VOTcSMUCJuaiHmiKiHTxKDaF2B3TIcye6bUQNjUxKYt4XJRTQ6bmoTHlRQIvK6nJmWfZDc1XS60RijjJCt1z7WtQBx4qf6T1orn1/I94oAlOMhi/TLN7M/TAGKxFgYCT004exqcwHyFA+4HqoXePf/5xyuB/fwqVzl/BJjq43am4Z/qZVaDLNbfefGfvm+8Agh6T98j9F98j6i5EUoOlcWa+RZn0bsYRAVD/ibNEF5Cceb5eoZ2Bi/WPPerGNIYWWVkACuFHE/kAwGA0IUbx7TUtH8AEOLKwh8SeGEWxoks1hff9sqH8OPh9h5/zFfhP/A+7Z63Vu4Y7t32vlnHd9Vy75n5DnnJ9XryQ7HwNXl7jz+2BcT+ee8pdx8Q76vl3u22DskmhB8ey8dX8VNtth/9GhANiAZEA6IB0YBoQDQgGhANiAZEA6IF9XsMTwPiF2yZ/+5WzRS6sOQ2KA8hb1WY8m57dUuda2PZ/bwSj7waVXOjWy8625nvH5C0CUTy/ffDNcwVJ/h5QCgc+/FzxhJ1RMoSiZIErzkN+j+4Dc49JVVSDlWUKZjSxVyuodS7ML+IM0ZDLNYe9a+Z9s4kK+TfQduJLpZcULgNBJ6aDcXgA5GsAcEWClVkgyRpbcfAD9nbE7K5jj/N+pZ8kbQW3JzboGftAJHzYvhT8K4oa0CECojaFGYORH1hzIEISyDCfXfETSCyr70GYtbhYewzp1br8VuSfFzAUKIAU6ET2WlmPugDucI2oL+5DKc2zqK+AaUOpNTBn2DHEvZPgS4FpVDOkrQxToS6sqhEEeecx07YFV1QjxVlOkAhJBbRFZEqcfuk9SBOJqPFGIqGBjgaaN07nFxJYYQTQ3BaOZdIymhcwAih1/SYsDI2kB4i1gfKYDqKU47PgJDxo9YGRAHfAdc3Sjz1rERRmAri+ZHK8/nKbQDnQHkISZgKU0ZWdDjkge8A0YQ+iZuSyPVjKrHEI3gNIakHRORnxjWF6acmMB0om9VL9ph5G/xstO1T+5FQbAIsxpBVQwM9QA7ZGQdMeWVkqfEoPvqid0RO2oso3hDM3cgAIssocJy9TUdv/Gx3duPHrdE57Lni+4bUqTiUlOsqwGRMHUyM26D5XBEmEabC1OBVTTGgRWG2HdClIHWNJEuWpeLBa6AWIPBAShl0Pq5DD4Qm97PjU+gd4fd1otQRRcbBYgxKg7lmAYJ5HVk8TYIH4nzVM/WOyNILk64QuQzcEUGBQGb8TWwW6AI1OYvipzIwyyTV5AHJ/dVAMBNizMXBxLgNxYqjAJFlaMtVBC0KBwSrDMA7ZKyB6JPeJ5CfEP2ncXlHzIDoV4Dg1D6ingOBcdeuswlIEKaHeppUQORgQMjJ2ouUHRCd+t+NcyC4p8MwAwIOKhrzKhAbU5M4mBi3QUkiXqqHh7anFHgR75l6qtMFdCkynFIYRJuawGtQPY7ic2JTU4rBMx100Hk2LZ+a3L4Bratih7Q/m5oICHF+YdUPM5dhIGDK1ONMjSSXXgzFTQYXj9YE1gdcW1hVRD41ma6I+qkoy6RmccjUdIJTSVmksACpg4lxG2BKpot1lHcD8xDgcggtiiHAwIz3mEIFK3UYJwK8Bml5hBoIFuvAHwUiL+7GdGCFEFqdkv8e4fblTpQ6xMlktBhtWWbYgsl7qcPJMM3t1GaUxTqWSNiPRXoxiJ8Mj82I6RaLNVM5Ey3WEP7yuiI6ftDtoIUZF27Vt0t4bNviMezxG3Ja+wC59XHSvkbFu1u6xa7I+Z6W1yOJG23k9I/adWzxGPb4DevHbgNxf2232BV0bz2+1ZHM23g/o+M32dqPfq37XzaSBkQDokXSuv8FI2lANCBaJK37XzCSBsR/aYB+tt7Hz//nB/inImhAfHSfd59RytU7z7dYhs9lIbQ+rteRlw0kV/dWvWkewYxH4n7DS3z+zOllscVKYWBd+RX5vm78m/SdQxz0abk5D6PUMy5HqZOcJp49pQck6TfivzkRRI4vlHQiUX2JYNgMN81IH7ECQlPpaQFE9soU/Du7pXXSBhDxLiCUW1IpJg+eOEBqyyndBiLeBEKfS6VhTFmfH81pXbF5T40DP3enjJ/dpeaMFBOAuME4ejcQi46l2at7gYgbQIQZEPX1nW48irh+R8Q1ICA9L09m69Elj2VfjQPApXyS3KPl5JzSwHzTVIaqT0CvwvMtwLaQJ/XzSR5ZFYWLC0JUcXcY1ncBZU5jV1JN6qICL5KTuLAoEObUgqRNN85UMIwHwUNV+A8kAB/wUO2JWzyNxs3g5IvmnQtj4scPlFTdDvOCmQMBbQKv0QEGBu3fVOPApDNwrzwQfEec+N8VfkgI6mGi6hN4Or960DtZ4pBULJKc00taEZkvfcB7mmkHSXvmXp7fB9sCuW3aex7ZHCPBg4Fg74tTi+V0vQqG40HIRCP8B7ZGOI9iiDDV3SfjZvi71BgTU08ktiCsDPOCocE848F2ya7lYO1TLjDxUO6ocYSySHC2HuQIWSOGfAEQc35I4AUkd8FED2ZA0FJ5TSw0hTsiKxBgQgixClceZuIzrwydOEkgjd9xabAoFDJ4qoiAVTKnFlLQAD/EGBqOB6FeF0i7MxCFToahuYobTKiB0ET9NAziiMK9L5CdeGBwR0z7rzbtmkYH+Bza2qoah7sjYs7X2RrRp8HuCOOH8Jvp0LgHBGgkekd4ILI2LDOq2iLx0IweCPZNwS3PXiR9UpkEBcKcWqCgsQQiC+siDDasslhXQAg3g9kUNjWtA5GdtwvdcQoE70s2NalGB/gcOjWtqXGEYATSmN1i7VgkS34IBwyugapPQK/C32rMYUgOCNW14KmmIoiI94jUhKmpeLTo1KRrBLEZogHR709NyoPwQNAHRAARPRCpSKIs7giZmuRuDUWnA5+K7VMTGBnBfWqSa7pPThZlTY0jlE9NPBX1809NFRCFHxJU3MnUJ6BX4fkW4DAIp+EChgTzFRIszLDoqPYEmIAoQwt7LB4tYnl2wb1DdeekFmnEZoBTi9VmEXgeBBU2/kPiNQaLdeLoUKor3wOwecYEAaHqG6rRITHwgssTU5IltOJh6FhgsNfVONwSgWV8hDKHfo/wJljGDwn3b9uuKPdtuXqVFx8mH6ttHld8IKr5x9dHt3sUOdY2c2uZPXrwWH0/+8NAXgXid/yJ4+f5G/v8kE8O/7PqbT/6fYWgGhBfJKgGxBcJqgHxRYJqQHyRoBoQXySoBsQXCaoB8UWC+i8C8d8J60vEE8Ov39Y6Hv/B6L7GhTE8DF3Ry9Ytb5Tk592cTbGqcPiOd/txzPbmjfam3zqv9IspYhuKvEI3jzBxpQlMCzz3VzIkrr6tX+Xq+ozNke8evTzvd9ptcK9EEgV5O74QZ9Dke8pVztxc3G13fKDtVICYt4eUVZTYhmLCEavzDAh9slWBmHutxI1YPP0gFhJBTcFIYUuTZGYV8rNAKN/BA7GBPmWHPwWI5XWmz3rzq3wvEH0FRHgIiPCzQDBvIxUdjaBsCEnn5IXSRmE+yIOtAoQdhxKHqnmAoHXikice8bkKBzK4qDG66cUYDuvRqdLGsr0aCMqJRafaQecppwQJKAXixA8Rl3ikVnWFmI8UUmqIQNVMfFxWstYk0d5ZSe43sx5yKkoWyoaQB99zeXjcSiRJl2oZuSP0uChxFOsAqGNwyQvlqeYqHLgC+2I7IHIojuGwHp0qbSzbo3wYMmNZphj4qsSi0MF9knToW1IZikz5taTxaK0KxGKkSnsmGeDjspK1JolO01aS+00LaBxUR0NumAwOhOpVeKUNdSyhElJGgNDjuBY7VvOAy0nRM+hzWKpwCD/ErGnkdjeGw3p0qrSxbI/IDCfQWBwQtTDFVPKKet5sjeipdxqP1qpAzEeKetBpz0REw8dlJZeaJF2lFSJAENNTdTSsq2EEw4MH3SltqGMJlxirO6IcjyYiwRocJiyRw1KFY35HlHm+MBzWo0tFkmLeHrNKBn9HQCAiak+KvInyV2yxLjSJUisIEsuR4mjHGggfl5VcapLgPN8DNJZVR8O6KkQa1qvwShviWBIWU1M5LlSZ3rgUNRBzFQ50HCwPr1BmDIf16FRpY9kex5TXgNCeQA4IUwnxVxwQvU1NqLW7xCJm4keKerA6NSUfX1rVJKl7IEB0ol0BJQtlQ5h9F4SXrAQtOinYYg3ukB2HEgfxE86jLkpFS+O6VOHQz+22IMtyVhgO69Gp0sayPfoege8FFBsDkU21I7KaBhglhb8S9HtEd1ESqNYaUgpKBfUjRT3QxVrVTHxcVnKpSdJVWiG+3/tfO97JW9iobfZ+zazyc1q+r74FWzx9zHf6/PDXvcX2sboTjwDx0YoX99Q3B+KjYvjy6h3tR7/W8d8ingZEA6LF0zr+BeNpQDQgWjyt418wngZEG6x/y7BRbB8QX3xnd38tE+TfA0SqX7uHYod5dzO/7ipFjW5Z47BVN1WwLL9aB7fV7Zbr3gGE1FCS5Hl5NIX95L07K+8cTxUQf19hZ8AqzQP9OlzFw6ckN2KbvzNaQj1WwhDR+Zf4kY/rQAzeO2W/jhx8LElk3OdnrdltbKXqfVzzoTSDkG0glvSIPK+37MdzrhSbKowPqaiuZ2OzRFUrGR4GItwFRNgBIrwLiCWEPwFEXr+m3w9EeAAI0/lwNjyDqG0YzyAGU6cwJoUyG8x7BccxOCssBcjrXMCDUNUO9U45CTNkrw7wPEg+XeITIOB/cuL3zoFlxjfRmrQNSsBcx5PyLgZlkpxG45nwo72ZH9ot/dNI1B9GeSo4j9U4Ljhfxe2phhP7ykBhQP0wlkCAVwLvmAjBfOYawOcjge9QHvYW9kHJKxeDA8pIgYlxHpcsBU07Jta6UNUONSjopYW9OnpJ9g+ifaBTEzQK+vJEv2aYPd/EahL3lURp9rekegesrJH0f+WZcHtZTQVwnkZC+iGepyIskCEIz0STwfye9UoAhN0Rb6zbcebcIZ66ztIL8V+CUkYSnw9iJkDPAreYMikK06JYfvAVc9VpZc5SUHMAzW1DtSPOZHP26mDOBFtsIL4CBO/LQoFRB5aab2I1YSP2RSciDWBWICLoiSjPhNUJslMYMY5GqrkkygI506hlXPGdjBcBkQsQdke8FRGI6X3KUvsMiDCmIoYQHTXSmBR23XkgxBtohaUAZxYAwdc/zhG1ll5cdvbqkIFhICi+Ggi5I4oDy4xvYjVJP+KgZ2X2iwEI+F95JgpEtoTqWAORC39DWCADUQGy9ZdHqBvUsmN+R4Si8sHjO9odMcEmU1PCLanMhEKDEvbBcmri470MYsVSqKYmVuFghgbbz2DYT+I7tVeHTk2XC+Krp6bTOJ+aKr5JqUnBJlYazqJzBiHvmBJH4T2VqUn674FwPBVhgVDpUPFUesjFIF4PBJxWclkloE/CQCS3WEcoY1zATFB1CmNSFHmr4r3Cx/ndnKUQQnFm4b0ZDA06J8M7JZePzdt1nMZOJLoQH8+vo/qfZLUqU42Qmm9SanKL9QVnwRnlJL1UkS5oNZ2F2VEv1ihX81TAAoFih9IrQ2GGENeD7kn7HsEfA/SeSfgeQSPDnPUx3McoMDGf2+XWP8zllT3rpe9lbWT3wXr9/FXnlfDx27KdXITlbn2zfjAeQvIeILZYCutArJe+l+mwBYSdv1bTgx2/e3S29nwwEMvufqXtP/CjXwOiAfGviKwB0YBoQDQgGhANiAbEF4+sAfFvBuLfP3D/OTZTA+LBLa/tTN5JxEp060Vdqbza4W5+nFUxyBt9m0fRva87nHD9e0oQnaffbsk5e95HjSuvjoJG2q3E4XsAF5U4kAZ5tz2Od0S7+trvTIVnYE3ElXpquQUPRC34UANBOYElEP7NXCEjyXPU9Rbn5h7y7CYxKIaS+smutAmn+5PyLNJ56+pCoyVPuQt48NR8WIyNkG6M+Y0Oz4AI9wIRHwciIzkT8j1A7HQkrPFJcpFLz4uo3gtEmAGRh1OwZFN4BIjOeBbj2WlsUM71NCqLQrUrOCMsnIZTxbYoQu3KwxjokVhfqhst+23KGydxceFaOYM3JdAGJFyuo2/LHjhW5oWkZMh7JSDnV3RCeD+SMZ3kA+dADKm4r4RzYYN4bkfRyrgotwURa+zqQmMlQTZSILogjI7CANF4QmF9nFVbBOk/SQOWFCIuAFLMAItC/VCQy0pecUKlFfTh7RzAwxhwR/Ql8RkcHwQsCGJvgBEBtxR4o0gyFo4Swp+oHg1P/jH6xPlg+KVA0x5J1iQZ6KD8FAaRn6bO5wFiKOBn+KmJ36fyyLpqZeQoR6hPppGgLjRWEjQAjS+KA43pcmQZTZ0seazogOR2maGQhd0g1MYBfIi3yg9FU/O14gRzI67wF5muQnZgAfHS1CZY+6LcEWBBIAediltKEpYHK31cYY6BNlMwz5ZOIgRzQoGAQ0MHy4GgQPBVfpVLizkcJ6YZBPGnUHZJEjZIJ/ob4HaU6KFikDRiBeKtGJVoSR7+wQOhowUGSBc0E95xa+Cb0DgXIDAxC6thDIVZ8Vb5odRA6OwYcYz9RcBhyLZYS6IcJhYqsJBEyKdQbsDBULqNMEk8EObZoiogYE4YEDQVgqERRgDBQ25kCMA1eCA09V/WiFGu3OBVNbohuuHvN4DItnwXIDAajgEyGiUBrQGIInoj/zpeg5zeg0WhfigOCCOZiGpFsW/pUyokmeSYEH0aSGBBgBA3FjOPIhEYAEHcEqqvAJFSsKlJW5KpKZU7IgRHCkq82mTRDgFXRVxVBgz/5WLLajLCEDggtVaGyAwhYlMNURcaKwnuln1qUi6JMkBysaFCRDxWmWlAtGCkoKwHsBrANxDdi+z9ULBY86ci4XokIWwqnwGOLJ24nGgpcWphNSTcKR0zQpQRAR8UAwJsiKHwJ1Iwz5aaOUH8jiGdi6pFJ3wM2h+F+ElsC10jhGQ2wsqmE5JmLGwQ6G8stTLE+Gz0qiHqQmMlmb14xfcI8EyI0WEMEJEjY82PMlaXVEisN7cc7t1ql8bl0c/7Bvtzuh27Sf70vrH4eC2RBxrf52EoD+kztp/TutiLa8uL9bNj+tSt/dbUOvxbxtWAaEC0uBoQDYgWV+vwF4+rAdGAaHG1rW1tw1YeZwVHQ38hiXh67epUOKon8+lp4bWNf7kshIB7fp3Jy/NubsuS3cZ+f84ycay8FB9DSPPYlv273SNVD7HyeXO0KgUIecKxRNHJo/Am/hBriYRhPpYi31CeqZ4DkXaB0PNkyO4Ewif0JTO3UkPcBCKlZQz7QBg1IO0CEYuXDFKo60D4Fg2IqvoT0i05+K5tARGWQISfAOI9dwSyio/eESn8IiDCLhBnSfiAmVADIXmvQXkH4CXY46/qNMIsBOc0EhKVPRVuhTEjcNy4IqaJ4c8DD+M0KmMCbJKTK38d5yVN2WK+n1gSpCFCnA64nhg/5MQpLG3FYqCE2HUqRYPk9EQksaNlkDDS9BVi1HrUM8bKh3KsK71yR5G0DJq74qEyILh7I3gH4vjgHghXpxEYjahSBV5H8SShsqZ6Ic/tl4RsLwlE1K/nwW+lLynGntkk+p7evaV5SVO2mO/P/Lw7Evk1PwT1ncfSSomBHlx/S28SI+Vqe+chU/hXnMIdmMES+FWvlgbCJYluPOjv3h6gT31VG/gbCfyHtTuiZ8ECTihe8bS/aVWo0wiS3+CBQPkCOhqxena/KFk4Pxbaqy4Rdh78Vkz/gvgTb0UPA+/mJU3ZYrGf+wpOR1zwQ8AeQSs+BgNCfVWU31LKCF/lLXVXlAMvhN1WREskuvJKs+G2mFsCFRE5qsNA/IfVqakAUQRNQg1EL0AoQS8rs2MGRFYlC+fH0qdcIrDzUnA2NOCQpLeihwERiHlJU7aY7ycg0gBOR1zwQ7JwRER3o8QgQMhc4fktWkb5Km9J5xTwQnhwRmh7RDceCkQWrgk8yMrRlPzUpEBE+eP1KvzUJMoYskYIEOW2zEGnJqs7mxOJ82M5jdkxNPS8JG4lSswiDxZoWciZfZmaSklTtpjvd0Bg0vH8kGAcFh+DTk3MECy8jlkZ4atMkfUoB16I2P+4qUmoQ9XUlPvT6GsDv+Ci9D8AMb0e6M951IVFvKyUQ3GR45iamJPBIlJwGhmJJRGLJ4mqWdhx58ci76R+OQ9+KyL5MzKH5KK2T6qrMS9pyhbz/cSSAIdQlljHDwEnRVpxMVCpgWd/jv6SgvFbShnhq5BUFtjm4IXw1CRckrMrL7RSXawvOfjaNra4or23fvzvKz6SqTrb39d52XqPf6fnrp9n+/JOGTuSw3YN22ftl85pu4xnaNTl1l9jhNz7m/E9uGEAmGf3znN/ZanHtj0Whj92D1tjPkKfEW/b2ta2trWtbW1rW9va1ra2ta1t/66tkXR+PyAqbYt043h9ZNRHbGnLZX/eLDuvb+8cPWPNb2ItshVmRVp/d7PkXp2r47JdbgOItfaqX1kLkyBV3U4bz8P5X2FzqSC7tlJVFvVZbfU5caV267CBEtcir12nc9wBYr9kdG9y2paciKu9WwJhghepHuJ5jXGxbw2IsANEeBiIsHHOGhCnlUtnJfK1wbgHiJWS/xgQpllBKRPVjlC+RBQ/E01qUErF8x30/JPzGxHdiYupT6DsdaTc8VnM5oldcfIeJcz5oL2e0VHULworxHQxurDOAAlSsissDO3bfklmYYgjS8VG4fTPtTxUrN4sde9OI2JXVQ4AocojRFIoPJaLZ7Gg/0WogKAhhoNqRyThGXA2VWkEwlnwfAc931ga+qqocpSyb8ymSJpAzHBWUY8S6GnQ3poJAh6FJkFVF0O9WTT9agwQeg3+CDupiO4Hv7tRcuAoUqhLCuckl6y5OFHMe8feCKo7Ut0RHGU2RxY8sl9YLEGG2GtW0P8CBN6z9kVSGsF0DSpnQdkHej5rVQTL1yZT5ShlPRAdTDOE9ZAFbPiR1EwQ8CiUFqC6GMYpqZkjyJ+XMoPqftxT8o2jALPDSgrHI78V7RFkzOe9S3JB5woIVSKJg3dkUScXhUZmea9ZUe4IVDhGGyTmRCgjKBcJE5yP0h4IZTlYWQ9EGAFEDh6I6Zzcp5oJoqYeWkKZGMop8cyRvhpeclIx5sftkgpEXVI4HnRHBNSmbkV179aBEGGJQEDk4Eao6HLUQECzoiduQVL1i15cRvrkiDHEWTiNxj7Q87PzGxHdCWE5WFkBItnU5D1KhKqTfe0Aglgc5Y4I0XEkHGuCyyqjRNVBTPeD390oeWGaQZrVKQwOnZqKo8u8dzUQGBlT+MB/Mun1jhEioktgPohmBf3JAdoRCRoWvOhGUZUQm7FLDsY+0PONpXEquhNgOVjZaaG/QCUDHw1OfJZ6lEBPo66dljjmelzOpUR0HAljTUh5YZSAP2JMDXA3bpWkcTyPUBKxksI5ucBbMRZHl3nvMGLgrFCGmv6Io2WCJkfhsZTFWvvvPkZ5xsQWt8DK7PMp7ENdCPv8jLVz8uJonn1M3WkTf+c7Wrq75PoIbJXD+7+vekeE9275g8q8T4dCdXG229uvFSXvYZTcX/L9I9B+a/oyWwOiAdG2BkQDom0NiAZE2xoQDYi2/aZA3A7jIwL9uTr+maH6sFbjXSUaEI+3um5qvPE6rGgOLOoa1pqbq3c8Mgx5o6VlHd3Y1Sem7VC3I+jujaJobqxva4ol81aHVHVpbjeRXcD55sjXda0PEX7ttPJbw7BmfHE/EPo8Z7lolkIo5d9tINafIsybJVNYDIDrc92uJxX8ciDkKfxVIOKHAYEnm2N996btwd6KYGv6fQcQae2OqIEYe8mIIVXhfT+AZVd5mlxH9QMp3iUXZTaAjcBHRERdh0i1M+gnZ6SPrJ0fP8B9oGPbNYI5YlyRrmpppZUAhgeeZIZqxkl4GqKrIdyNMGxFoCwRnLcbhWhunMvZ59H/WL/WLnLZtl0uQXLEfXm+XrgJnBkbVKMCjiFv5gcCN5CkTz9rypKzZr1mbafmRnAw3pLSAOp2piuDU6Zxt8YBqddBBCkgpFBaWmklgOHRq71Bn+X1ILoawhhJBMR6BMoS6UtrW1EELsnPqMvZOoIgJq21izviPJ7lWew3n2fNle8HLzPB+6icr28uNZ7FDcQUM4xupaYWJVF4fRO/kXk7U0BK5tmp8Y1BOYvfSQ56NaG2lVacakIo+iGZzTugqwE+yNTPYSuCuo69KLqrTk04mzgg3Wxqmrc7vyMWQOTi+8FVEDtDPHSgYOGBUKaG8wvZAALaGR4Ibacehq0aMQR5gCpHLvN0DYRrxe6InuYUpPfptdSkDixjDFsRzOrYjaI4jOHsYCNoQNTtrizWAkRvjiqpXAMgvNCagCMVEFCoCNn5hchZxfGkDFFfJo1ct6PDEHdrJI0LlDu5iU9bWmmFh+YkblsTEFyOXhMHpVyZ/O9WBLM6dqMo7jFydkqVKkFetrsCBJgHWPa874fc7qyEoY4hsEJDI9C4UOcR5Sf4xdpxGJgBwXZoQ90ODQOzRYa9GmmBhBpGdh8F6sXatyIMDzYOo0PC0mCeBnQ1hDEysrfQagSzOnajwDwfy9mxWqzpe8S83e3vETtb3ju2U0Fc+WC5/Bg3P7bvQZJ36ljUFPJML2Mt9v0I5nX83IhYiXd9n99rdp9T8Z6fOO5hadzXSg55Vy/Df8TeimBex8+OiJb4xT+stN+avkirDYgGxFcdkgbEFxmSBsQXGZIGxBcZkgbEFxmSBsQXGZIGxBcZks1W/w/YsxJ4MoiENwAAAABJRU5ErkJggg==","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":"iVBORw0KGgoAAAANSUhEUgAABiAAAABYAgMAAAAB0NeIAAAACVBMVEX///8AAACAgIBJX6m2AAARO0lEQVR42u1dS47jug5lBAcQPHIBVXMhI8OryAVqARlU9mP0KMvwMLirfCIPqY/jpFK/7vS7dqE7/lAUpWORcXjCEN2xHY/0V2yPbec3WLcC8Rus+/dMFNJRqC/6pMLz/jC207/n4C8l27qNtwM3Bj+MtXwb/4L15i8tCosH7RX7TZMv7Cw1uMlNPzW1rLlVy8LM9lCZGArrrikrGnnd81eACCF4RwxEKen0n7VxchpNfWwRQpZHj+4OIFTjWNp5DQjW7i6AUAviVo2p2nc0v+ZuTPzcWNbspIWfAVHN5OgvgPAL9+x9QATB1S8BMTNfteNK8L0v5S+ACNeAoA8D4bKd1WA9XQdi+fhuIOiDQAQTuALEbmrj324aJuwNU1xyvH/Ca1Qx8pTkFQFJfY0yrAU62lM8PrEOvh78zpfy/D+bjn7iWhmxx/vcmmV3sCK+snWttnSqH/1A2jSydhrb0ewsR8FAYGzq4sxq7cWutWpHm3rk1532eZYeB+0PvZNoBhDl+OYz6fRqaR16HGSPtSkQctfKGtY9gSvu97inj0ftbmQghjObYHe6F1ck68qjTYSqZx18Pa6hE+RUXtaUi0fRa/kwOjkjfxFkLzI9G+JFhteOrikn67GHzko6iN008opwaTx8Ng7SDzxU9U8Chlottnro96Ra0xnY7qUPvvwLPZrugLOs2VZEn6yZz6TTq6V16FEmRG5vc01+OLdnvndsL+J1DmoOq8ACr2KEByCe79UzHBzasATr4OvDOfS1PExniUCDtI8y3EsYzgaeF415+rml06nifky61QmUdiMvfrWTRyF7g44qR8lWrWZbW75bz5iWlrhNq/20VVsA0Sbd6J3E9uSaxMqlmXR6VFqHHtkKBqJNMcIHNdb2KA9bgIBzq2KED2RABKwIyveUTE6hJcsbENLTGMSbSp/Y7wGE9UMjJKsVkaRpykAgWCc74zWv76ZqIHQdSzuaHNl4aeI2ckaulG0BhGqMurX3FJvy+JZmUmJEPCqtQ4/QzpozENGl8ErqscfD7YO6JgZCnFu5ImyCvQDRqxZpw8esIyQ3UsiTuSaRDaNOiUwtr4STuCbRWACR1sbOpn7UdYCFLvBzjICd6pp0dSkQigSPyUsPxU3ja9dkt2ANRJZD73k0eXxLMxmB8NUsZttEonJNMbCc4pQixJzk/XH07QjWWQU/R6jLmLgNv7Inbk8a7qQNH7OOVieNnyNM3kuIGhDU47lRzrQSwjmwkgRrBmSQwE2h1ZYDQrq+KYB0Cv+TgEhmJwKlK4K1J3ua4DE56YH1stY4mUWwxpm2astzyfa6CXOltqbR5PEtzSQFXM1AmG1OJFjznW/bft8Ta3jnnWS4ee1verLmB+kvqvjJ7b17IzyInV8H4k+p+FuGugKxAvHXD3AFYgViBWIFYgViBWIFYgViBWIFYjXxL7d0BeJrW5i98pZ4E35JNsylKmH59JIyf2KBg+Gr4WnCIGhrZNY4aSMfjgXRkEgApQXuirVhsSdaGOc9503PO0yMq1yTtt73SzN5FQh3FxBultbHETIIvsiMu0LCZtTNgEgpeEufasKHPz12aoe7AoS7AMJ9Ggi3fMcUQLiZ0ks2S80Pqcf/Q0DQEhBUG+suzatXRAC3xnQbEP0XgKBPA0G3gFAmhl+UcVfaunqGloDYJUaDMjeUVcDpD2VYSPq/nUZvTIWSyeCYLqacCv4DASZIe8nMCheCpeT6KNlh1n9i/ge3zPwG40JkIHYe95kkY07S4zi3YNnanfBAkJ7J7ApSW3kPySBK6aVxxiAJAdoT5+OULW1p0LajZA1nbJYTdBhbI88SlSOpx9FLohEcBDAbNHXnwbAgj/R/zKb5PrEsmIHBqb5B6CTcold3AiAk70WSvYpX+pSndmmlcJIQLTO/wbgQDITEiDGc0EJSmL2kGMfaAk0mVdZydq8XHggSlqTXeiXJedmDC8Rty3LjjEESoonCADHOR58tBSHAZqVmszCrDjqCz07YKVujGEk9DuR+wUEAC4M5CiQ8CmYpZDbHL2U3oMVAw9kIVtwCzInWKGF+OOuKUGaHE5ZEjhEB2e7AZ43fkLgQzIeiHb/2BRBgP4xzC1q6tLYlyz4PZ+TKcY0os0UgBV4Hy/2aMUgwcW3ifPDozVLuDXPxy2hFic0SZQN0tDoDeZaoHEk9jl6BCClIMkcBDbw5iT51OVnzoEwG7oJbgDlBk3pDH9KEpxUhrIsaCOlrMn5D4kJA/5iDtU6rDHduAWyqrSVZkbIiEjMRHBBli8BLT0FXFOs2IIwgoZ5h8ongkC11iUNSAqFsFt+qvcb7yLNkQATy83HshNShLqHvlaNAci96MDN69rLBn07BXBOFRLjSLnxmQRgvxFwQYkTlmkS/Ty0trV5xIXirgegB3tyCZWu5xU64g+Ka5Jqw80YQ0trRXJNxVU4zBom5ksT56LOl6gh1VmZslj6oDnWdfZ6lwjXNxxGUkthaABWOAgmPwiPwnEjCHzdAYPMEpgPYc4O0EP7CJNyKEWFwkOsxeMY+hgksjFZCMlgizP8AkTINL/EnWB/cigAhdEcOcXAMtQVXrGX/Lm8aGBhcCxr4PQfSHKxPYFYw7adkkFjYT5yPU7aUxwBLAVfFZuFQLTqUrXHKs6REyxMsr8Zx7Y1c9abuXpbu4lu4UBGQl7bvfV7N1obZ6MKV/fu3+yz92Hx9wKC7eTdXenivj+8FIlt7C4jPbfdZ+rX5+oPb+lnTOrz/W0tXIFYgVktXIFYgVktXIFYgVkvX4T22pd9t3Gf0fbwNt/id01r39SM9r0D8HBCzr+kH2QnXu/D4xBPHkjDhzxvP+FYZZ9rc+O+Z0yLxf/4cUj6ZH0bewyedvD/EtONwHsbjcc6FCLdsDf7awPy1pmgxxH/2vby8lX3HD68n+zQYo9PvvsmV1DnaJZmljUdFTfyTrVN7j2/8//YWEHWSfwkIXwzJOBhOzaf09VbLLfjgNKEieQr+xBlJy5A/+3c0clpo5HRkfS+EpLecJN1Gl4CYcSvkW4j1iWwhZ58534ZUVElCcAUoPo8BkyFHLbWpxEW+H+x7s5f9kYwqzr8AwXgcj01HtJFLm/dXxN1AhLK+iSU3CyDwtVnKSc+QTNf8MCTCLSCubKNW4SgNvwpEnuwExEx7Cbcka2sgxG6kcUsg3BUgqARiH6EQIDbXgdgVSxD1NHb6VVdwIErWxC59DRZVLIzZwFncs2TjCN+PvhOI3QIQbaqdwQ6t5FbMa2bE3qQKh3BMlFuR+RzGmMC/QVuhhSMAAQaHjPgEvsVO3Yx8HZ4Tr0qgQDqHfFHWxWtyaRSZnhK7A7PDuoS9EfuLE77fHrru5UD74/HljSHYHrYHBmJ7IFSo6HOKUr6GjWoZSDDOWRNeK11YBZ7MwXAx79sLe4OCcDA8qmFgmiUmSFaOgbAr8eiEfUmvjwAi+PLOqrgVs5oZjlCFY/TeG7ci8zkywwRkDLRCC85Ac4YMDA5hXQSslj59yX6HrFvAd6HZSp59lHUpHZIjkTlR4cx66a8HKyaOildC13Rd9E+drYh43DAQXVN4OOMTWfoc9TSCn7MmhBGhdSQAhHEwGAjLMhfLWlgLtiJ2qH+hS1zO99jX9LoAgZ6y/83cinnNDEeowvELZAQt2WJ8jswwgb1a4GHE7cOWS3EW/wvrVIEAI4W0eAuX/6JUi4CPtKwLVoT0CBllKxYz6bRuCFZEY0CkFZGAoBtAoJ5GBiIUFSisjgSAMA6GrQhZ1GMwp0OVaxoL1zQmxgQq1SQgQvKvoKAU1TnmNTM0WBsQgajkc0QnFJQ1kSbGl8G6BIIz6kpzEI6G9Ai/XwDhVUZjhL7rUirHIhBBY0SnQOQYcQ0IdTrKQZB6GuqaCtaEJ3NgoFUlDoYAgRghxW+i01EgQgFESECEXNuDdysgfOmbMrfCXdbMGFGF43TyiVtR8DnGUIzKewMCtZ30XZMwOMwqVwResS2gwFcGIqiMq1xT0Mysr5y8AuHxrmkPIJqGbrimHKyNx4B6GgjWJWsC1TOc1pzIpUhaeRIIEnPgjpTnUNR5kmU/gUY2CJXGniMQJfAcMWgljfxWItk0XtTMCKjCwaM1bkXB5wjtZJbzeNAKLdJzhDA4hlTzY5gsBA/GOzlzvxash7NyUsb89NCOIsMraso8FAddXEZMniP2HJxfNwd7jkCwjsEbwfprm5s9TAVafv92/5b1XdarWGaB1FU4ciujj11rMX8M/DxbxX1gVJdH37TdAOIb9H2ljVGMr7WYt/pJ9sX6WRPdD8RPbisQKxArECsQKxArECsQKxArECsQKxArED8IxHea9Ri0k9KK4/GvIe2sQPzhbSs8gu0beAYv+BywuS5Zb93tKWhMgF+aWrhZ2Mv7Klw2ye1rO5ouplniv82e6OWN/zb7+PkyyYhUiwDhyRI9l5vPDI7M5AgXYvlT4ctrIWmqtdy5bSR9/dIBCP1kvNkszNmmSHQbN2Q+t5s5EPuMh01kU7w0nwJiMxONF+KHy3y94xZNHEbH6RdL1zcVED5cYFDVBnHV1NZlHHwiVmQlxh7R+h+JUDAnQdwPRByFAUGfBILuB6JbBMIUdh8DYtN1kl4RIDj7AiD2dnsIEMF+GeYzQBSyMyCMOFOviMufzbntmA4FEPuYzms4WbF93dJT/J8kdfEUkxdPB0jyEZ99iX+4Bpk3ldvK0fH4dICeDeuR9pyl3R5wxNeY0/CEtMhr1oCzfPVJXrsOfcg5tZgt2aoe6H06NN3TgYF4Eh1yO+05T5x5E8o2GQEEeBaZkaIMDi0Ybymf8ndUwCLBl6GLuiFB2SOjSJH9Dg34IMZ2abX6x62tARDbjs3e0AsPp+meN/QsK56Tec8xuddIUo+Tew0PspEVIdcg8088wXJI/8VUYJP1sPSz3L7Sumk0RQjNscdnEg18jB746rO8dnv02JjTTG070SPWiK3xX4xwz2xRAqIpeBNgm+hvvyjPwn57BSlhzYMLD0VTsqN9NR3VPvArKr74jRiu2OHIvoiPH2SgXH/DWzkB/97S2L4BiA1TPOLkPsVwt33TocLnypB5SALUWyOt+EyUE3ewjWf/abiVABGPWVfWw9KYxk5ayTXe44mUqd2+sQb0tBVHJpMc/71oj00K0Nwb22EyyTsBqoakT1sRmTcBtgmSnbkCDjNKjK2CvLVW1ADph6xqRqu1RTzlzL3oCVZjIZV38Ln+Rqu6a1bK4tY1GQj2rx24BpjA/SUQ4A3aGQDBgdKmkSXoIPeitKT9s9677ELSBEsbOjwrEKwJGvistFUg2B7ML1leN7bciAR0sV5i7TMgECMybwJsk2ArIpUi0l918ZmfKN59yr/5At7XJRCiaxEIq78hbJf8Gyo3gXjOQOyZFRjt7p75rEwKJqlpmgQEpo79BsvJ4OGa+Khpkmsi6GFfz9KduSbCUdcwpyH6dm7/zG3/SWe57Z5dk9zPgBdrSm6N7JoaWxHxmgRostumUyC6zJsAyWEsXJP9GVtF+Rt95mfY76jsJlT7KICwuiEAwptr0hIyfXJN1se7wfr1RQJmAqKRUP3KsWKrgTgOPN6lFqDFNb1uDhuWkwDJMvtm+8pyCMQMBPTwux9hLjQItI0cSQsOpLRBsH4VDYdOmA7cds8BmCUaOd43wnTYarBGLxu1RlgQMajzc8RWzvEeniMyb6IGwup9KCNlQsUMUDXzz6FZPY2g1T4o/cZKqhtyEv6JBmuwNnL9Deg2/sud2+WTZ9fQJ7dSV3drPd6pb/NB+UsrvvZkHd6VcPRt26Whn2fbfDcQX7fip4H4IbMfSdcjAPHHzH4kXSsQD6JrBeJBdK1APIiuFYgH0bUC8SC6ViAeRNcKxIPo+m8A8T+nxm4X7vEyAgAAAABJRU5ErkJggg==","mimeType":"image/png","cols":196,"rows":71,"chars":769,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"}],"totalChars":112098,"truncatedChars":0,"text":"# User ¶█Complete the assignment below, thoroughly:█# Target█Existing integration/e2e tests and historical hazard documentation covering daemon refresh with attached PTYs. Non-goal: source-wide architecture summary.█# Change█Locate the closest real-process test harness that can start broker/brain, attach multiple controllers, perform daemon refresh or brain replacement, and assert bidirectional PTY traffic afterward. Identify how to extend or compose it into a deterministic feedback loop without touching live user sessions.█# Acceptance█Return exact tests/helpers/commands, limitations, and the smallest deterministic pass/fail scenario matching the reported symptom. No edits or live refreshes.█# Assistant ¶█_**Planning investigation of diagnose skill and globbing tests**_█# Tool call ¶█//Reading 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 required documents█glob(path=\"CONTEXT.md;docs/KNOWN-HAZARDS.md;docs/DEBUG-ROLLOUT.md;docs/adr/**/*;docs/ADR*;**/*refresh*;**/*integration*;**/*e2e*\", hidden=true, gitignore=true, limit=300)█<out>█\u000eCONTEXT.md█# docs-site/src/harness-contract/█integration-checklist.md█# crates/█## spt/tests/█projindex_reader_e2e.rs█dummy_harness_e2e.rs█projindex_writer_e2e.rs█whoami_identity_e2e.rs█daemon_refresh_e2e.rs█composite_e2e.rs█docs_bundle_e2e.rs█release_verify_e2e.rs█bootstrap_verb_e2e.rs█trial_drain_drive_e2e.rs█resume_no_control_steal_e2e.rs█send_stamp_agent_id_e2e.rs█endpoint_autostart_e2e.rs█list_json_liveness_parity_e2e.rs█listen_seed_retry_e2e.rs█active_only_never_relay_e2e.rs█attach_wedge_e2e.rs█worker_visibility_e2e.rs█worker_lifecycle_e2e.rs█psyche_download_e2e.rs█notify_shell_e2e.rs█multi_subnet_bringup_e2e.rs█live_adapt_translation_swap_e2e.rs█tunnel_e2e.rs█shell_sleepwake_e2e.rs█shell_e2e.rs█shell_actgate_e2e.rs█run_no_dup_session_e2e.rs█resume_template_e2e.rs█ready_resume_ledger_e2e.rs█quickstart_e2e.rs█psyche_sid_custody_e2e.rs█poll_envelope_e2e.rs█oneliner_e2e.rs█nested_resolution_e2e.rs█live_resolve_e2e.rs█live_firsthost_e2e.rs█live_bind_firsthost_e2e.rs█livehost_bootgate_e2e.rs█job_escape_e2e.rs█idle_edge_drain_e2e.rs█gateway_owner_shell_e2e.rs█gateway_e2e.rs█drive_e2e.rs█contract_e2e.rs█boundary_ready_strand_e2e.rs█bind_honest_cross_perch_e2e.rs█bind_cwd_project_e2e [...1150ch elided...] 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 refresh tests█grep(pattern=\"daemon refresh|brain replacement|replace.*brain|refresh.*daemon|DaemonRefresh|refreshDaemon\", path=\"tests;test;src;docs;crates;packages\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# docs/█## KNOWN-HAZARDS.md█620|- **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 e...█*621|- **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 ...█622|- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 mult [...8144ch elided...] cmd = Command::new(&spt_bin);█186| cmd.no_window()█...█216| eprintln!(█*217| \"=== W4 daemon refresh: run_ok={} online={online} harness_pid={harness_pid:?} \\█218| refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\█219| broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\█220| still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\█...█260| refresh.status.success(),█*261| \"REQ-DAEMON-REFRESH: `spt daemon refresh` must exit 0 against a live daemon \\█262| (stdout: {refresh_stdout}; stderr: {refresh_stderr}).\\n\\█263| === brain stderr ===\\n{brain_stderr}\"█264| );█Skipped missing paths: test, packages█[Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Finding PTY harnesses█grep(pattern=\"broker|brain|controller|attach|pty|bidirectional|real.process|child_process|spawn\", path=\"tests;test;src;docs;crates;packages\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# docs/█## BROKER-BRAIN-SPLIT-RESTORATION.md█*1|# Broker/Brain Split Restoration - Design Rationale (pre-ADR)█2|█*3|**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` - see §0); six amendments folded in (marked **[V1]**-**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 - restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...█4|█5|**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.█6|█...█12|█*13|**Verdict: the core holds.** Confirmed at source - broker is an in-process thread (no broker child-spawn exists anywhere in the repo); no doc/plan/commit marks the collapse deliberate (silent drift; regression framing correct); `update.rs:233-234`'s \"exec the new binary's brain\" is aspirational and never wired; `applyhost.rs:176` records `applied` before the handoff (the optimistic `applied.json` watched on `enlyzeam`); `applyhost.rs:238-239` re- [...48504ch elided...] || 2 | `spt-term digest_parses_a_real_pty_stream` | 3× gravity (2026-06-03; 2026-06-04 runs 26957386850, 26966738231) | sprint-collapse assert (`digest.rs`) - PTY input echo and the child's write are **concurrent writers** into the master-read stream; back-to-back `send_line`s let a later line's echo interleave mid-`Write(...)`, breaking the tool regex for that turn | HARDENED (2026-06-04): sends serialized - each line pumped back (adaptive echo-copy count, 2s/line bound) before the next, so no concu...█12|| 3 | `spt-daemon sync.rs torn_pull` + `concurrent_writes` | 1× (2026-06-03) | `wait_for_stream` deadline too tight under load | HARDENED: `wait_for_stream_except` 2s->10s; no recurrence |█[Showing lines 1-336 of 706 (49.6KB limit). Use :337 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Assistant ¶█_**Planning targeted test module discovery**_█# Tool call ¶█//Reading domain ownership rules█read(path=\"CONTEXT.md\", selector=\"1-220\")█<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**: [...45094ch 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-223 of 883. Use :224 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading refresh E2E█read(path=\"crates/spt/tests/daemon_refresh_e2e.rs\", selector=\"1-340\")█<out>█\u000e//! THE-FORKENING W4 - `spt daemon refresh` on a LIVE daemon with a hosted█//! endpoint (REQ-DAEMON-REFRESH): the brain generation cycles (new brain pid,█//! generation bumped) with NO binary swap, while the broker process, the hosted█//! harness session, and the endpoint's ONLINE perch all survive untouched - the█//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI█//! verb. The recovery story: wedged brain-held state no longer needs a full█//! daemon bounce that kills every PTY.█//!█//! Real `spt daemon run` broker + supervised brain + a REAL dummy-harness█//! `[session.self]` session (the live_adapt_translation_swap_e2e rig, minus the█//! translation child). Bounded everywhere; every spawned pid is reaped SCOPED█//! at test end.█//█// [int->REQ-DAEMON-REFRESH]█use std::path::{Path, PathBuf};█use std::process::{Child, Command, Output, Stdio};█use std::time::{Duration, Instant};█mod common;█use common::CommandNoWindowExt;█use spt_store::perch::{self, ParentHint};█/// Scoped pid-tree kill (cleanup) - never machine-wide.█fn kill_pid(pid: u32) {█#[cfg(windows)]█let _ = Command::new(\"taskkill\")█.no_window()█.args([\"/PID\", &pid.to_st [...10099ch elided...] }) - the supervisor cycle, not a re-stamp.\\n\\█=== brain stderr ===\\n{brain_stderr}\"█);█assert!(█broker_still_running,█\"REQ-DAEMON-REFRESH: the broker process must survive the refresh untouched \\█(pid {broker_pid}).\\n=== brain stderr ===\\n{brain_stderr}\"█);█assert!(█harness_alive_after,█\"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\█refresh - only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\█=== brain stderr ===\\n{brain_stderr}\"█);█assert!(█still_online,█\"REQ-DAEMON-REFRESH: the endpoint perch must remain ONLINE across the refresh \\█(the session was never torn down).\\n=== brain stderr ===\\n{brain_stderr}\"█);█}█\u000f█</out>█# Tool call ¶█//Reading broker hazard history█read(path=\"docs/KNOWN-HAZARDS.md\", selector=\"624-710\")█<out>█\u000e## 7. Boundary & delivery integrity (added 2026-05-31 - Stage A red-team)█...█### 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]`█- **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...█...█- **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-f [...21763ch elided...] scoped guard - the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-safe-decline (pid-alive AND basename==psyche-program AND cmdline contains `<id>-psyche`; any unreadable signal DECLINES - a missed dup is bounded, a wrong-kill catastrophic). The orphan-leak half of perri F-010xF-015 (the psyche own-copy is the other half, ADR-...█...█<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->█[Showing lines 623-713 of 736. Use :714 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading rollout safety rules█read(path=\"docs/DEBUG-ROLLOUT.md\", selector=\"1-180\")█<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 split decision█read(path=\"docs/adr/0018-broker-brain-process-isolation-restoration.md\", selector=\"1-240\")█<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 ¶█//Locating restart PTY tests█grep(pattern=\"brain_restart_survives|survives.*brain.*restart|brain.*restart.*gapless|attach_survives_target|controller.*restart|restart.*controller|multi.*controller|second.*controller\", path=\"crates\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## src/█### attach.rs█248| // session output (broker->serve) - including the controller's synchronous,█*249| // inline, multi-frame ring replay (`become_controller`), which holds the█250| // broker's single-threaded per-conn handler for the whole replay. If the█251| // forwards rode `brain` too, that stuck handler could not read them ->█252| // serve->broker backs up while broker->serve replay backs up -> mutual IPC█### brain.rs█1126| /// Query the RUNNING broker's own compiled image version (F-025). The broker█*1127| /// survives `spt update apply` (only the brain restarts - ADR-0018 D3-3), so█1128| /// this is the authoritative \"what version is actually executing\" - read from█1129| /// the broker's compile-time constant, NEVER inferred from disk. Returns█1130| /// `None` when the broker is too old to answer: an older broker replies█### broker.rs█5505| ///█*5506| /// Post-fix the second `become_controller` bumps the shared epoch BEFORE█5507| /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing█5508| /// any frame. The wire shows exactly `[2]` - the current-screen repaint - never█550 [...4561ch elided...] _name();█82| let broker = Broker::bind(&name).expect(\"bind broker\");█### netstream.rs█144|#[test]█*145|fn receiver_brain_restart_is_gapless_and_exactly_once() {█146| const TOTAL: u64 = 20;█147| const KILL_AT: u64 = 5; // receiver dies after reading this many chunks█148|█### presence.rs█82|#[test]█*83|fn presence_survives_brain_restart_with_cursor_resume() {█84| let dir = tempfile::tempdir().expect(\"tempdir\");█85| let name_b = unique_name();█86| let _broker_b = net_broker(&name_b, dir.path());█### xfer.rs█232|#[test]█*233|fn push_survives_target_brain_restart_exactly_once() {█234| let dir = tempfile::tempdir().expect(\"tempdir\");█235| let (name_a, name_b) = (unique_name(), unique_name());█236| let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\u000f█</out>█# Tool call ¶█//Locating process harness helpers█grep(pattern=\"struct Test|fn spawn|fn start|spawn_broker|start_broker|spawn_brain|start_brain|Command::new|controller\", path=\"crates/spt-daemon/tests;crates/spt/tests;crates/spt-broker/tests;crates/spt-broker/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/tests/█### attach.rs█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.█...█470|// already-producing session - exactly the endpoint-run shape, whose transient█*471|// cold-start spawn conn drops after `Spawned`), `become_controller` replays MANY█472|// frames into the SERVING brain while `serve_attach` must forward each back to the█473|// operator. Before the two-conn split both rode ONE broker IPC connection, so the█474|// broker's single-threaded per-conn handler - stuck mid-replay (broker->serve) -█...█584|// screen and paints; a fresh endpoint-resolved cold attach (from_seq=0) must land█*585|// on the controller's legacy-baseline path (which accepts the repaint's forward█586|// pseudo-seq without a reject-gap fatal), render the alt viewport, and NOT dump the█587|// main-screen scrollback (the exact #6/#7/#8 corruption). Proves the two seams█588|// compose over the wire, not just in isolation. Unix-only: the child emits raw VT█...█1 [...46098ch elided...] ])█57| .output();█58| #[cfg(unix)]█*59| let _ = std::process::Command::new(\"kill\")█60| .args([\"-9\", &pid.to_string()])█61| .output();█62|}█## spt/tests/█### active_only_never_relay_e2e.rs█35|fn send_bounded(spt_bin: &std::path::Path, home: &std::path::Path, args: &[&str], body: &str) -> Output {█*36| let mut child = Command::new(spt_bin)█37| .no_window()█38| .args(args)█39| .env(\"SPT_HOME\", home)█...█83| bound: true,█*84| controller_node: None,█85| harness_only: false,█86| adapter: None,█87| recent_projects: Vec::new(),█Showing files 1-20 of 78+. Use skip=20 for the next page, or narrow paths/pattern.█Skipped missing paths: crates/spt-broker/tests, crates/spt-broker/src\u000f█</out>█# Tool call ¶█//Mapping broker test suites█glob(path=\"crates/*/tests/*.rs;crates/*/src/tests/*.rs;crates/spt-broker/**/*test*.rs;crates/spt-daemon/**/*test*.rs\", hidden=true, gitignore=true, limit=500)█<out>█\u000e# crates/█## spt/tests/█projindex_reader_e2e.rs█dummy_harness_e2e.rs█projindex_writer_e2e.rs█json_emit.rs█whoami_identity_e2e.rs█daemon_refresh_e2e.rs█composite_e2e.rs█docs_bundle_e2e.rs█release_verify_e2e.rs█bootstrap_verb_e2e.rs█trial_drain_drive_e2e.rs█resume_no_control_steal_e2e.rs█send_stamp_agent_id_e2e.rs█endpoint_autostart_e2e.rs█list_json_liveness_parity_e2e.rs█listen_seed_retry_e2e.rs█active_only_never_relay_e2e.rs█n1_pairing.rs█attach_wedge_e2e.rs█worker_visibility_e2e.rs█worker_lifecycle_e2e.rs█twohost_cli.rs█translate_proof.rs█psyche_download_e2e.rs█notify_shell_e2e.rs█multi_subnet_bringup_e2e.rs█live_adapt_translation_swap_e2e.rs█brain_survive.rs█tunnel_e2e.rs█shell_sleepwake_e2e.rs█shell_e2e.rs█shell_actgate_e2e.rs█run_no_dup_session_e2e.rs█resume_template_e2e.rs█ready_resume_ledger_e2e.rs█quickstart_e2e.rs█psyche_sid_custody_e2e.rs█poll_envelope_e2e.rs█oneliner_e2e.rs█nested_resolution_e2e.rs█n1_compat.rs█live_resolve_e2e.rs█live_firsthost_e2e.rs█live_bind_firsthost_e2e.rs█livehost_bootgate_e2e.rs█job_escape_e2e.rs█idle_edge_drain_e2e.rs█gateway_owner_shell_e2e.rs█gateway_e2e.rs█drive_e2e.rs█contract_e2e.rs█brain_split.rs█brain_respawn_rename.rs█boundary_ready_stran [...347ch elided...] igin_spanning.rs█wake_single_flight.rs█endpoint_survival.rs█xfer.rs█wanmsg.rs█twohost.rs█replicate.rs█sync.rs█notifsync.rs█netstream.rs█netbroker.rs█input_ack_deadlock.rs█idempotent.rs█dispatch.rs█daemon_e2e.rs█broker.rs█attach.rs█access.rs█shellchan.rs█seedproofx.rs█rosterprop.rs█resume.rs█reseed.rs█psyche_residency_expectation_e2e.rs█psyche_event_turn_e2e.rs█psyche_context_file_e2e.rs█presence.rs█pairjoin.rs█mesh.rs█legacy_resident_sweep_e2e.rs█handoff.rs█driven_by_selfheal.rs█digest.rs█daemon_lifecycle_real_brain.rs█control_stamp_lifetime.rs█budget.rs█brain_swap.rs█### src/█test_home.rs█## spt-store/tests/█carrier_claim_int.rs█## spt-term/tests/█winspawn.rs█surface.rs█stream.rs█inject.rs█dsr.rs█## spt-msg/tests/█killer_quickstart.rs█Skipped missing paths: crates/spt-broker/**/*test*.rs\u000f█</out>█# Assistant ¶█_**Identifying real-process test harness candidates**_█# Tool call ¶█//Reading endpoint survival harness█read(path=\"crates/spt-daemon/tests/endpoint_survival.rs\", selector=\"1-430\")█<out>█\u000e//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) - daemon-restart endpoint█//! survival, against a REAL broker over the real local IPC.█//!█//! ## The bug this gate locks down (seed #6, the field-accept blocker)█//!█//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every█//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)█//! though their records survived (info.json status + adapter + cwd + a ledger row). The█//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's█//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted█//! endpoint the restart orphaned, resuming from its last ledger session.█//!█//! ## What this rig proves█//!█//! A fresh broker (an empty session table - exactly the state after a `daemon stop`)█//! plus a seeded previously-ONLINE, controllable (spt-hosted), relay-less, dead-custody█//! endpoint with a ledger row -> the pass RE-SPAWNS its harness into a live broker PTY█//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays█//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this [...5758ch elided...] c::new(),█};█brain█.sessions()█.map(|r| r.sessions.into_iter().map(|s| s.endpoint).collect())█.unwrap_or_default()█}█/// The broker session id hosting the \"survivor\" endpoint, if any (for teardown).█fn survivor_session_id(name: &str) -> Option<u64> {█let mut brain = Brain::cold_start(name, now_ms()).ok()?;█brain█.sessions()█.ok()?█.sessions█.into_iter()█.find(|s| s.endpoint == \"survivor\")█.map(|s| s.session_id)█}█fn kill_pid(pid: u32) {█#[cfg(windows)]█let _ = std::process::Command::new(\"taskkill\")█.args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])█.output();█#[cfg(unix)]█let _ = std::process::Command::new(\"kill\")█.args([\"-9\", &pid.to_string()])█.output();█}█\u000f█</out>█# Tool call ¶█//Reading process survival harness█read(path=\"crates/spt/tests/brain_survive.rs\", selector=\"1-420\")█<out>█\u000e//! Restoration D7-1 (ADR-0018 V5) - **process-level survival across a binary█//! swap**: a hosted PTY child + a live QUIC connection survive a brain-**PROCESS**█//! restart that lands on a **swapped on-disk binary**, with the broker holding█//! both endpoints untouched throughout. This is the `int` E2E that re-points the█//! regression-masked REQ-DAEMON-2 / REQ-UPD-3 evidence off the in-process█//! `brain_swap.rs` shape onto a real process boundary.█//!█//! ## The proof is a TRIANGLE (doyle D7 vet, call 1) - this is leg (b)█//!█//! No single test proves the whole claim; three legs do, and this file is honest█//! only when read alongside the other two:█//!█//! - **Leg (a) - the real-broker-process leg: `crates/spt/tests/brain_split.rs`.**█//! The REAL `spt daemon run` broker *process* survives a brain kill and respawns█//! it. That is where the broker's own process-packaging is proven.█//! - **Leg (b) - the survival leg: THIS test.** A real brain *child process* +█//! a real on-disk binary swap + real broker-**held** endpoints (a PTY child and█//! a live QUIC conn). The broker is modeled **in-process** - faithful, not a█//! weakening: the broker is the never-restarting an [...16098ch elided...] advanced = true;█break;█}█thread::sleep(Duration::from_millis(50));█}█if !advanced {█teardown_panic(█&stop,█\"the held PTY child produced no output for a post-swap marker - endpoint held but DEAD\",█);█}█// -- Teardown: stop the supervisor (kills the live brain child), reap. --█stop.store(true, Ordering::SeqCst);█let _ = sup.join();█// The hosted child dies with the broker drop at end-of-scope.█let _ = broker;█}█/// Raise `stop` (so the supervisor thread kills any live brain child and exits)█/// and panic with `msg` - keeps a failing assertion from orphaning a brain.█fn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {█stop.store(true, Ordering::SeqCst);█panic!(\"{msg}\");█}█\u000f█</out>█# Tool call ¶█//Reading brain split E2E█read(path=\"crates/spt/tests/brain_split.rs\", selector=\"1-430\")█<out>█\u000e//! Restoration D1 process-level smoke (ADR-0018 Q2/Q3) - the broker/brain█//! process boundary, proven against the REAL `spt daemon run` binary.█//!█//! This is the de-risk spike #1 in production topology: the broker is the█//! always-up per-machine anchor that spawns + supervises a `spt daemon brain`█//! child, and **the broker survives the brain dying** - the supervisor respawns█//! the brain, which reconnects to the still-serving broker socket. A respawned█//! brain (a NEW pid in `brain.ready`) can only appear if the broker stayed alive█//! to accept its connection, so the respawn IS the broker-survival proof.█//!█//! D1 is a true minimal skeleton: the brain child connects, signals ready, and█//! idles; the logic loops stay broker-side until D2. So this test asserts only█//! the boundary - come-up, survive-brain-kill, respawn - not any logic.█//!█//! The full PTY-child + live-QUIC survival across a swap is the D7 `int` E2E█//! (REQ-UPD-3 re-point); here the process-level boundary is `unit`-tagged.█// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█use std::path::PathBuf;█use std::process::{Child, Command};█use std::thread;█use std::time::{Duration, Instant};█mod common;█use commo [...6380ch elided...] xpect a NEW pid ------█kill_pid(pid1);█let pid2 = wait_for_new_pid(&ready, pid1, Duration::from_secs(30)).unwrap_or_else(|| {█kill_pid(pid1);█let _ = broker.kill();█panic!(\"supervisor did not respawn the brain after it was killed\");█});█assert_ne!(pid1, pid2, \"the respawned brain is a fresh process\");█// The respawn proves broker survival (a new brain could only connect to a█// still-serving broker socket); assert the broker process itself too.█assert!(█matches!(broker.try_wait(), Ok(None)),█\"broker must survive the brain death (it is the always-up anchor)\"█);█// -- teardown ---------------------------------------------------------------█kill_pid(pid2);█let _ = broker.kill();█let _ = broker.wait();█}█\u000f█</out>█# Tool call ¶█//Reading daemon lifecycle restart test█read(path=\"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\", selector=\"1-420\")█<out>█\u000e//! M11-W0.4 (i) production-path int (REQ-DAEMON-1) - re-points the█//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which█//! constructs a [`BrainLifecycle`] directly - green coverage over an UNWIRED█//! production path, the M3-era false-green) ONTO the actual brain entry point:█//!█//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,█//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS a real ONLINE live█//! endpoint and DRIVES its pulse loop - proven by the loop ingesting a commune into█//! the live-context tier. The reconcile that hosts is the REAL brain's, not an in-█//! test `reconcile_once` call. A cold `run_brain` over an already-online perch IS█//! the post-brain-restart rehost: the perch's `status=online` survives the brain█//! process, and the fresh brain re-hosts the lifecycle at boot (daemon-authoritative█//! liveness, CONTEXT:194).█//!█//! **F-030 W1:** the Psyche is now a PER-EVENT TURN, not a resident process, so this█//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)█//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop█//! ingesting the commune - u [...3375ch elided...] join(format!(\"{id}-commune.md\")),█\"Source: self-commune\\nreal-brain note\",█)█.unwrap();█let ctx = spt_store::contextstore::live_context_file(&home.path().join(\"tracked\"), id);█let deadline = Instant::now() + Duration::from_secs(25);█loop {█if std::fs::read_to_string(&ctx)█.map(|s| s.contains(\"real-brain note\"))█.unwrap_or(false)█{█break;█}█assert!(█Instant::now() < deadline,█\"the real brain's pulse loop must ingest the commune into the live context tier\"█);█thread::sleep(Duration::from_millis(100));█}█// No resident psyche to reap (F-030 W1 - host_one spawns none). The run_brain +█// serve threads are detached and die with the test process.█}█\u000f█</out>█# Tool call ¶█//Reading daemon E2E flow█read(path=\"crates/spt-daemon/tests/daemon_e2e.rs\", selector=\"1-360\")█<out>█\u000e//! B9 daemon end-to-end - the M3b milestone proof, driving the whole consolidated█//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's█//! lifecycle as in-process loops **and** survives a brain restart with its hosted█//! PTY child + output stream intact and gapless.█//!█//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):█//!█//! 1. **spawn** - a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,█//! spawns the session, and round-trips a marker through it (the session is live).█//! 2. **Psyche-loop** - a [`BrainLifecycle`] (the consolidated re-host of the█//! interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;█//! the daemon marks the hosted perch `online`, so its liveness is the daemon█//! `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).█//! 3. **commune** - a commune drop lands in the watched dir and a scheduled pulse█//! tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).█//! 4. **brain-restart-survives** - the brain is dropped (a crash) and handed off█//! repeatedly; across each handoff the child's pid is unchanged, exactly one█//! ses [...11609ch elided...] he Self's ready marker\");█// The hosted Psyche is marked offline on teardown (the online-stamp counterpart).█BrainLifecycle::mark_offline(&psyche_perch);█assert!(█!is_perch_alive(&psyche_perch),█\"mark_offline flips the perch offline\"█);█// -- Reap: kill the child and confirm the broker drops the session -----------█now += 1;█let mut brain = handoff_retry(&broker_name, state, now);█brain.kill_session().expect(\"kill child\");█drop(brain);█let mut reaped = false;█for _ in 0..200 {█if broker.session_count() == 0 {█reaped = true;█break;█}█thread::sleep(Duration::from_millis(10));█}█assert!(█reaped,█\"child exit must reap the session from the broker table\"█);█}█\u000f█</out>█# Tool call ¶█//Reading attach restart flow█read(path=\"crates/spt-daemon/tests/attach.rs\", selector=\"740-880,1030-1405\")█<out>█\u000efn broker_spawns_the_pty_child_in_the_requested_cwd() {█...█let (program, args) = (█\"cmd\".to_string(),█vec![\"/c\".to_string(), \"cd & findstr \\\".\\\"\".to_string()],█);█let sid = brain█.spawn_session(SpawnReq {█program,█args,█rows: 24,█cols: 80,█endpoint: String::new(),█cwd: Some(proj.to_string_lossy().into_owned()),█env: Default::default(),█translation_binary: None,█adapter: String::new(),█install_dir: None,█})█.expect(\"spawn cwd-printing child in the project cwd\");█brain.attach(sid, 0).expect(\"attach to read output\");█let mut out = Vec::new();█brain█.read_output_until(leaf.as_bytes(), &mut out)█.expect(\"child prints its cwd\");█assert!(█count(&out, leaf.as_bytes()) >= 1,█\"the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}\",█String::from_utf8_lossy(&out)█);█brain.kill_session().expect(\"cleanup kill\");█}█// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain█// crash mid-attach: the successor re-serves the [...23588ch elided...] reads exactly ONE output chunk, so its█// consumption cursor (`next_seq`) sits at 1 while the broker's ring holds more█// - the worst-case stale floor the bug hinged on (`prior.next_seq` is the█// CONSUMPTION cursor, not the successor connection's delivered state).█let mut driver = connect_retry(&name);█let sid = driver.spawn_session(echo_spawn_req()).expect(\"spawn echo\");█driver.send_effect(MintedOp::new(Minter::Rc, 1), b\"SEED_ONE\\r\").expect(\"seed input\");█// Read one Output event to advance the driver's next_seq to 1.█loop {█match driver.read_event().expect(\"driver event\") {█BrokerEvent::Output { .. } => break,█BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),█_ => {}█}█}█...█}\u000f█</out>█# Tool call ¶█//Reading refresh root cause history█read(path=\"docs/UPDATE-WEDGE-ROOT-CAUSE.md\", selector=\"1-320\")█<out>█\u000ePath 'docs/UPDATE-WEDGE-ROOT-CAUSE.md' not found\u000f█</out>█# Tool call ¶█//Finding refresh stall history█grep(pattern=\"refresh|update apply|spt update|detach|reattach|rc|stall|most.*PTY|session PTY|controlled session\", path=\"docs/UPDATE-WEDGE-ROOT-CAUSE.md;docs/KNOWN-HAZARDS.md;docs/FAULT-MATRIX.md;docs/adr/0018-broker-brain-process-isolation-restoration.md;crates/spt/tests;crates/spt-daemon/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# docs/█## KNOWN-HAZARDS.md█4|█*5|**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...█6|█7|---█8|█...█14|- **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.█*15|- **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.█16|█17|### 1.2 Poll-rewrite race & info.json mid-write reads█18|- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it -> torn read, misclassification.█...█20|- **spt-core mapping:** the daemon owns both writer and reader -> use in-proc [...48971ch elided...] ...█165|█*166| // -- (3) Register the gh_release adapter `cc` at 1.0.0 (its install dir is█*167| // the registered source_dir - the in-place update target). --█168| std::env::set_var(\"SPT_HOME\", &home);█*169| let install_dir = spt_store::perch::spt_home().join(\"srcs\").join(\"cc\");█*170| std::fs::create_dir_all(&install_dir).unwrap();█*171| std::fs::write(install_dir.join(\"manifest.toml\"), adapter_manifest(\"1.0.0\")).unwrap();█*172| spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &install_dir, 1000)█173| .unwrap();█174| std::env::remove_var(\"SPT_HOME\");█175|█*176| // -- (4) ONE invocation: bare `spt update`. --█177| let out = {█[Showing lines 1-743 of 1108 (50.0KB limit). Use :744 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Assistant ¶█_**Investigating output filename mismatch**_█# Tool call ¶█//Locating update incident docs█glob(path=\"docs/**/*WEDGE*;docs/**/*wedge*;docs/**/*UPDATE*ROOT*;docs/**/*RCA*;docs/**/*update*\", hidden=true, gitignore=true, limit=300)█<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█## adr/█0025-live-daemon-coordinated-adapter-update.md█0029-adapter-update-arc-resolve-not-execute.md█0016-platform-targeted-update-sets.md█0004-single-daemon-broker-brain-split-and-self-update.md█## design/█v0.16.0-update-arc-and-cli.md\u000f█</out>█# Tool call ¶█//Finding incident citations█grep(pattern=\"UPDATE-WEDGE|22:47|v0\\\\.27|v0\\\\.28|refresh.*detach|detach.*refresh|reattach.*refresh|most.*session|all.*session.*except|until.*rc\", path=\"docs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# docs/█## DEFERRED.md█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...█*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...█37|| **Origin-source update bootstrap (`spt update fetch` - pull the latest signed release from GitHub into the existing verify->stage->apply pipeline)**: t [...23056ch elided...] node in the selected lab subnet that is pinned to `debug`; maintainer tooling may accept an explicit node list for targeted tests. It watches until every expected debug-pinned reachable node reports the target debug version applied, or returns a timeout table with per-node state such as not pinned, offline, staged awaiting consent, blocked by live broker resources, applied, or rejected.█46|█47|The debug channel sequence is maintainer-tooling state, not project truth or node identity. It may live in an ignored source-repo local file such as under `target/`; losing it is recoverable by choosing a higher debug version. Deployed nodes only require that the next accepted debug metadata version is greater than the current debug version they have seen.█48|█[Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Reading real daemon attach harness█read(path=\"crates/spt/tests/attach_wedge_e2e.rs\", selector=\"1-360\")█<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 update composite harness█read(path=\"crates/spt/tests/composite_e2e.rs\", selector=\"1-300\")█<out>█\u000e//! THE-FORKENING W4 - the bare `spt update` composite, end to end█//! (REQ-UPDATE-DEFAULT-COMPOSITE): ONE invocation fetches + applies the core█//! release, THEN updates a registered gh_release adapter - core-first order,█//! both legs landing observably. Hermetic: a fake `gh` CLI (`gh_fixture`, a█//! sibling `[[bin]]` copied to `gh(.exe)` on a PATH-prepended dir) serves the█//! whole release channel from a local fixture dir, so the mandated gh carrier█//! runs for real - subprocess, argv shapes, asset staging - with no network█//! and no real gh auth. The trust anchor is a test key overlaid via█//! `release-keys.json`; the core apply is the daemonless path (no daemon in█//! this home), which swaps THIS test's private copy of the spt binary (never█//! the shared target/debug artifact).█//!█//! NOTE: the file/binary name deliberately avoids the substring \"update\" -█//! Windows' installer-detection heuristic forces UAC elevation on a█//! manifest-less exe whose name contains it (os error 740; the█//! post_step_fixture precedent).█//█// [int->REQ-UPDATE-DEFAULT-COMPOSITE]█use ed25519_dalek::{Signer, SigningKey};█use std::collections::BTreeMap;█use std::path::{Path, PathBuf};█use std [...7881ch elided...] e pull: {stderr}\"█);█let landed = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).unwrap();█assert!(█landed.contains(\"version = \\\"1.1.0\\\"\"),█\"the registered install dir must hold the new manifest: {landed}\"█);█// -- (7) CORE-FIRST order (the composite doctrine): the set download█// happens strictly before the adapter's latest-release query. --█let set_at = log█.find(\"--pattern update-set.json\")█.expect(\"the core leg downloaded the update set via gh\");█let adapter_at = log█.find(\"api repos/u/r/releases/latest\")█.expect(\"the adapters leg queried the adapter release via gh\");█assert!(█set_at < adapter_at,█\"core leg must run before the adapters leg (core-first): \\n{log}\"█);█}█\u000f█</out>█# Tool call ¶█//Reading brain restart hazard harness█read(path=\"crates/spt-daemon/tests/brain_decouple.rs\", selector=\"1-430\")█<out>█\u000e//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) - the suspend-brain█//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.█//!█//! ## The bug this gate locks down (the update-wedge root)█//!█//! A controller's writer thread does a BLOCKING socket write to its brain█//! subscriber connection. When that brain is SUSPENDED (doyle's rig:█//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the█//! output-driven eviction (`append` -> `mark_controller_gone`, bounded by the 5 s█//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`█//! only caught an EXITED writer - so a controller BLOCKED on an idle session was█//! evicted by neither: control latched, reattach was refused, `rc --take` hung,█//! `daemon status` still read healthy.█//!█//! ## Faithful in-process analog of a suspended brain█//!█//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact█//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv█//! buffer, the controller channel fills, and A's writer thread parks INSIDE█//! `write_frame` - precisely the state a suspended brain leav [...16345ch elided...] er's socket█/// so its writer parks INSIDE `write_frame` - the exact black-hole the RCA pinned.█/// Sibling of [`flood_spawn_req`], but FINITE + input-triggered (the flood child█/// never stops, so it can't prove a frozen cursor / bounded high-water advance).█#[cfg(windows)]█fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {█SpawnReq {█program: \"powershell\".to_string(),█args: vec![█\"-NoProfile\".to_string(),█\"-Command\".to_string(),█\"Write-Output SEED; $null = Read-Host; \\█foreach ($i in 1..1400) { Write-Output ('B' * 1023) }; $null = Read-Host\"█.to_string(),█],█rows: 24,█cols: 80,█endpoint: endpoint.to_string(),█...█}█}█[Showing lines 1-433 of 834. Use :434 to continue]\u000f█</out>█# Tool call ¶█//Reading resume deadlock harness█read(path=\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\", selector=\"1-390\")█<out>█\u000e//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) -█//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).█//!█//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER█//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions█//! onto the brain's OWN request/reply conn. A conn's send half is one█//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads█//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING█//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.█//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output -█//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`█//! calls. When an actively-streaming session backs the conn up, the brain's own█//! subscriber writer blocks holding `send.lock()` -> the heartbeat REPLY is starved ->█//! the heartbeat stalls -> the brain never drains -> SELF-DEADLOCK. That is the field█//! wedge (both severities: respawn replay-burst + steady-state output backup), with█//! NO dead peer required.█//!█//! RED-first discipline: these tests ass [...14367ch elided...]█// reply-read loop also drains the replay so no viewer writer blocks/evicts. State█// is settled on return - no timing window.█hazard_brain█.sessions()█.expect(\"barrier round-trip flushes the fire-and-forget subscribes\");█for &sid in &sids {█let count = broker.test_session_viewer_count(sid);█assert!(█matches!(count, Some(c) if c >= 1),█\"seam check: a subscribing resume MUST attach >= 1 viewer to session {sid} \\█(got {count:?}). A 0/None here means the viewer-count seam does not observe \\█subscriptions, which would make the Some(0) positive assertion above vacuous.\"█);█}█for pid in pids {█kill_pid(pid);█}█drop(conns);█drop(brain);█drop(hazard_brain);█}█\u000f█</out>█# Tool call ¶█//Finding executable test commands█grep(pattern=\"cargo (test|nextest)|daemon_refresh_e2e|brain_survive|attach_survives_target|controller_restart_with_viewer|brain_decouple|brain_resume_conn_deadlock|attach_wedge_e2e\", path=\".github;docs;Makefile*;justfile;Cargo.toml;crates\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# .github/workflows/█## ci.yml█156| run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"█*157| # `cargo nextest` runs every test across ALL test binaries in ONE parallel█*158| # pool - `cargo test` runs the binaries SERIALLY (only threads WITHIN a█159| # binary). nextest also builds the test targets, so the old standalone `cargo█160| # build --all-targets` is dropped (clippy --all-targets below still compiles█161| # every target - examples/benches included - so non-test breakage is caught).█...█173| - name: Test - Phase A (light pool, full parallel)█*174| run: cargo nextest run --workspace --no-fail-fast -E 'not ( (package(spt-daemon) & kind(test) & binary(/^(attach|brain_swap|broker|budget|conn_blackhole_lifecycle|daemon_e2e|daemon_lifecycle_real_brain|digest|dispatch|driven_by_selfheal|handoff|idempotent|inject_control_wedge|input_ack_deadlock|net_worker_starve|resume|shellchan|two_origin_spanning|twohost)$/)) | (package(spt) & kind(test) & binary(/^(attach_wedge_e2e|bind_cwd_project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_spl...█175| - name: Test - Phase B (heavy class, serialized, on a now-qui [...33250ch elided...] control_stamp_lifetime -- --test-threads=1`.█40|//!█*41|//! Do NOT run a bare `cargo test --test control_stamp_lifetime` on Windows.█42|█43|use std::collections::BTreeSet;█44|use std::sync::atomic::{AtomicU32, Ordering};█### daemon_e2e.rs█37|// daemon-hosted-liveness), which it still exercises directly.█*38|// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration█39|// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*█40|// brain-restart survival, not the process-level swap. Process-level endpoint█41|// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.█Showing files 1-20 of 44. Use skip=20 for the next page, or narrow paths/pattern.█Skipped missing paths: justfile█[Some lines truncated to 512 chars]\u000f█</out>█# Assistant ¶█_**Planning daemon refresh test harness**█**Designing multi-session bidirectional test**_█# Tool call ¶█//Reading update incident triage█read(path=\"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\", selector=\"1-110\")█<out>█\u000e# LIFECYCLE-TRUTH - milestone triage + JIT plan (operator-called 2026-07-07)█Field-hardening milestone off the 2026-07-06/07 incident night: the v0.27->v0.28 update wedge,█the mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake█double-spawn. Every root below is PINNED (rig-reproduced or line-level) - evidence ledger in█doyle's findings backlog (seeds #5-#10) and this doc's per-wave notes. Owner: doyle (gate),█executor: todlando. Waves ordered by field pressure × dependency; **mint ALL REQs█`required_stages=[]` first, activate per wave as started** (traceable rule 5).█Gate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,█nextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change ->█grep tests for OLD behavior as assertions, shared-seam -> run ALL seam tests, gate suites█FOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag - the earmarked█REQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.█---█## W1 - psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)█**REQ-ECHO-DROP-DIR-RESOLVE** (impl, unit)█Root: `fire_e [...5824ch elided...] fecycle█**REQ-SPAWN-COLLISION-GUARD-LIVE-DUP** (impl, unit, int)█Root (perri parentage + recovered filing): the WAKE/RESUME respawn seam processed ONE wake█TWICE within 1s - broker (306368) spawned two identical `launch --cli ccs --id flynn --resume█<sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. Damage mode:█duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a█fresh /clear rotation -> injects routed to the contended record and lost). Fix: single-flight█wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second█...█Int: two concurrent wake requests -> exactly one launch tree.█...█...█(bump-ON-milestone-branch pattern, deployah drives).█[Showing lines 1-113 of 219. Use :114 to continue]\u000f█</out>█# Tool call ¶█//Reading deadlock code analysis█read(path=\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\", selector=\"1-160\")█<out>█\u000e# UPDATE-WEDGE round-3 CODE-READ - reap-drive exonerated; the seam is a SharedSend self-deadlock on the brain conn (todlando, 2026-07-09)█Report-before-fix per RULING 2 §5/§8. doyle asked: pin what the counter-54 reap-drive█(ce053dc) blocks on each tick, especially whether an UNRESPONSIVE peer lengthens the tick█>15s. Answer: **it doesn't block on anything - and the real wedge is a different seam the█reap-drive only widened.**█## 1. The reap-drive does NOT block (doyle's dead-peer-tick hypothesis - FALSIFIED by code)█`run_brain`'s heartbeat loop (brainproc.rs:239-278) per tick: `thread::sleep(500ms)` ->█`brain.net_status()` -> spawn-consumers-once -> `brain.sessions()` (the counter-54 reap-drive,█brainproc.rs:276) -> `write_ready`. It hosts NO PTY sessions, so it does not itself drain PTY█output. Neither IPC call blocks on a dead peer:█- **`net_status()` is a cheap field read.** `dispatch_net_status` (broker.rs:3573) returns█`node_id_hex()` / `addr_json()` / `conn_count()` - no `bounded_block_on`, no peer wait.█`conn_count` takes `shared.conns.lock()` only briefly; a dead-peer connect never reaches█`register_conn`, so it never holds that lock.█- **`sessions()` reap takes each [...5563ch elided...]█The mechanism is a code-proven deadlock, so the rig CONFIRMS + GUARDS rather than discovers.█brain+broker+PTY (`brain_decouple` template): N sessions actively producing output, the brain█resume-subscribed onto its request/reply conn; RED-first - the heartbeat stalls and an█active-streaming controller is stall-evicted >15s WITHOUT a dead peer needing to be present█(proving dead-peer-independence). Assert BOTH severities (respawn interleave + steady-state█output backup). Fix A/B must turn both green. Dead-peer arm optional (to show it is not the█trigger).█## 5. Sequence█Reported. doyle to rule the shape (A vs B) -> mint the REQ (candidate█`REQ-BRAIN-CONN-NO-SUBSCRIBER-DEADLOCK` or similar) -> build the confirmer rig RED-first -> fix ->█both severities green. No REQ / no fix until doyle rules.█\u000f█</out>█# Tool call ¶█//Reading incident rig verdict█read(path=\"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\", selector=\"1-130\")█<out>█\u000e# UPDATE-WEDGE round-3 RIG VERDICT - net-runtime starvation seam FALSIFIED (todlando, 2026-07-09)█Report-before-fix per `docs/UPDATE-WEDGE-2-ROUND3-RULING.md` §7. Rig lives in the█`uwedge2` worktree: `crates/spt-daemon/tests/net_worker_starve.rs` + a throwaway█DIAGNOSTIC probe on `crates/spt-daemon/src/nethost.rs` (`active_dial_tasks()`,█`net_canary_age_ms()`, `dial_permits_available()` - no REQ, marked throwaway).█## (a) Mechanism confirmed? - NO. FALSIFIED, robustly, across both dead-peer shapes.█The ruling's decider (§5): do dead-peer `submit_dial`s HOLD/serialize the broker█net runtime's 2 worker threads (starving the reactor that services LOCAL loopback█`rc` I/O), or do the QUIC awaits YIELD? **They yield.**█Rig: dialer with a 3s QUIC-op bound; fire K=12 concurrent `submit_dial`s straight█onto the broker's own `NetHost` (the exact pump path); read the reactor-liveness█canary (a runtime task stamping epoch-ms every 25ms - a stale stamp == workers█monopolized).█| dead-peer shape | K in-flight | peak canary age | dial permits free |█|---|---|---|---|█| proof-hang (accepts QUIC handshake, black-holes the seed-proof - netbroker.rs dead peer) | 12 | **32 ms** | 12 |█| unreachab [...3220ch elided...] und-2-deferred stagger.** The new█brain runs net-consumer setup + initial peer dials interleaved with entering its█drain loop; resumed controllers go undrained across the dead-peer-dial window ->█stall-evict on respawn.█Next rig (needs doyle's authorization - brain+broker+PTY stack, `brain_decouple`█template + a real pump round with a dead peer): assert whether (1) the counter-54█reap-drive and/or (2) startup sequencing blocks the brain from draining an█actively-streaming controller >15s. That is the real decider now. The pure-NetHost█Stage-1 rig has done its job: it removed the net runtime from suspicion.█## Runner█`cargo nextest run -p spt-daemon --test net_worker_starve` (or `--test-threads=1`).█Hermetic (relays disabled, no discovery, loopback QUIC); no SPT_HOME/PTY needed.█\u000f█</out>█# Tool call ¶█//Reading promotion gate report█read(path=\"docs/W3-WAVE-GATE-REPORT.md\", selector=\"1-100\")█<out>█\u000e# LIFECYCLE-TRUTH W3 - WAVE GATE REPORT (todlando, 2026-07-07)█**Wave head: `e75e856`** (branch `lifecycle-truth`, pushed). W3 COMPLETE - all 6 REQs built.█Requesting doyle's gate. Local gate GREEN; Linux leg is doyle's/deployah's.█## W3 ledger (all pushed)█| REQ | SHA | stages |█|-----|-----|--------|█| REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL | `049b2be` | impl+unit+int |█| REQ-DAEMON-STDERR-PERSIST | `32c789e` | impl+unit |█| REQ-DAEMON-STOP-LIVE-SESSION-WARN | `ff6ebeb` (+int fix `8931680`) | impl+unit+int |█| REQ-RC-RECONNECT-TRUTH | `3d632b9` | impl+unit+int |█| REQ-UPDATE-ONE-SHOT-FINISH | `e85da91` | impl+unit |█| REQ-DIGEST-GENERATION-SUPERSEDE | `7e37e37` | impl+unit+int |█| **REQ-UPDATE-PROMOTE-DRAINED** | **`e75e856`** | **impl+unit+int** |█## REQ-UPDATE-PROMOTE-DRAINED @e75e856 (mechanic-d - closes the W2 15s residual, KH 7.36)█**Root closed:** W2's stall-evict only BOUNDED the update-apply false-promote window to█`BRAIN_WRITE_DEADLINE` (15s); a new brain could signal `brain.ready` inside that window█while the OLD generation's local controller conn was still wedged -> apply \"promoted\" onto█a frozen control plane (the 22:47 incident). `brain.ready = subscribers drained`.█[...2880ch elided...] set): OK.█- traceable-reqs check: exit 0; REQ-UPDATE-PROMOTE-DRAINED `+impl +unit +int`.█- Doyle staged condition - grep OLD rc auto-start assertions: CLEAN (every `rc.rs` reference█asserts the NEW connect-only truth; the other hits are unrelated `api` startup hygiene).█## Gate gotcha (for the isolated-worktree rig)█Full-workspace nextest leaks `target\\debug\\spt.exe` dev-daemons from e2e teardown gaps -> a█follow-up build hits `os error 5` (Access denied) removing the pinned exe. Kill SCOPED (only█processes whose Path == `<this repo>\\target\\debug\\spt.exe`; NEVER the `AppData\\Local\\spt-core\\bin`█live plugin listeners) between build steps. (Seen twice this gate; killed 8 then 4.)█## KH 7.36█Updated: the W2 residual is now **CLOSED by W3** (`REQ-UPDATE-PROMOTE-DRAINED`), not just bounded.█\u000f█</out>█# Tool call ¶█//Finding dummy PTY behavior█grep(pattern=\"DUMMY_HARNESS_TICK|heartbeat-ms|mode.*dummy|stdin|read_line|read_to|echo\", path=\"crates/mock-adapter;crates\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## applyhost.rs█852|█*853| fn echo_spawn_req() -> SpawnReq {█854| #[cfg(unix)]█855| let (program, args) = (\"cat\".to_string(), Vec::<String>::new());█856| #[cfg(windows)]█...█884| let mut seat = cold_connect_retry(&name);█*885| let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");█886| let child_pid = broker.session_pid(sid);█887| drop(seat); // the seat brain detaches; the broker keeps the child█888|█...█1015| let mut seat = cold_connect_retry(&name);█*1016| let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");█1017| let child_pid = broker.session_pid(sid);█1018| drop(seat);█1019|█## attach.rs█56|/// served viewport is a **driver** - attaching wakes the endpoint, the█*57|/// viewport ending detaches it (active -> dormant; the transition echo is█58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like█59|/// [`mark_driven_by`]: a session served without a perch has no resting█60|/// record to move.█...█139|/// b4 made the controller a non-blocking `try_send` that DROPS frames when its█*140|/// bounded channel is full (a con [...19124ch elided...] atrix). The happy path already█113| // emits this XON before `{commit}`; an adapter that ABORTS mid-sequence█...█122| // XON (`ctrl+q` = 0x11): resume the terminal output flow the `ctrl+s` (XOFF)█*123| // chord paused, so the payload's echo flushes deterministically on a ConPTY█*124| // (the int test observes byte-receipt via that echo). A real stash/restore█125| // adapter restores flow at the end of its sequence just the same; this keeps█126| // the `ctrl+s` chord (and its real 0x13 mapping + floor exercise) intact while█127| // making the payload's arrival observable, per the test-specialist's latitude.█Showing files 1-20 of 204+. Use skip=20 for the next page, or narrow paths/pattern.█Skipped missing paths: crates/mock-adapter\u000f█</out>█# Tool call ¶█//Finding refresh command test hooks█grep(pattern=\"refresh_brain|request_brain_restart|BrainRestart|cmd_daemon_refresh|daemon.*refresh\", path=\"crates/spt-daemon;crates/spt\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/█### src/█#### applyhost.rs█28|//! half-swapped seat.█*29|//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],█30|//! ADR-0018 D3-3): the broker signals its supervisor to cycle the brain█31|//! child onto the just-swapped binary with `start-reason=update`. No hosted█32|//! endpoint terminates or suspends (REQ-UPD-3, absolute for this class - the█...█338| // promotion + auto-rollback on a brain that fails to come up is D6.█*339| match brain.request_brain_restart() {█340| Ok(_honored) => Ok(ApplyStagedOutcome::AppliedPending {█341| version,█342| product_version,█...█352|/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging█*353|/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the█354|/// broker and raise the same supervisor brain-restart signal the post-swap█*355|/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) - brain stop ->█356|/// respawn -> readiness trial -> promote, broker + every hosted PTY untouched by█357|/// construction. The recovery verb for wedged brain-held state that previously█358|/// needed a full d [...14965ch elided...] 00|use spt_daemon::endpoint::broker_socket_name;█101|use spt_daemon::frame::{Envelope, Role};█...█399| thread::spawn(move || {█*400| let restart = BrainRestart::new();█401| supervise_brain(█402| &stop_sup,█403| &restart,█#### trial_drain_drive_e2e.rs█108|use spt_daemon::brain::Brain;█*109|use spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};█110|use spt_daemon::codec::{read_frame, write_frame};█111|use spt_daemon::endpoint::broker_socket_name;█112|use spt_daemon::frame::{Envelope, Role};█...█373| thread::spawn(move || {█*374| let restart = BrainRestart::new();█375| supervise_brain(█376| &stop_sup,█377| &restart,\u000f█</out>█# Tool call ¶█//Reading CI heavy command fully█read(path=\".github/workflows/ci.yml\", selector=\"150-180\")█<out>█\u000e - uses: actions/checkout@v5█# Linux: link with mold (gcc -fuse-ld=mold). The slow part of a Linux█# build is serial linking of the many test/bin targets; mold parallelizes█# it. Linux-only + CI-only (set into the job env, never committed to█# .cargo/config), so Windows (MSVC linker) and Linux devs without mold are█# untouched. See ci-kitsubito-speedup-plan.█- name: Use mold linker (Linux)█if: runner.os == 'Linux'█run: echo 'RUSTFLAGS=-C link-arg=-fuse-ld=mold' >> \"$GITHUB_ENV\"█# `cargo nextest` runs every test across ALL test binaries in ONE parallel█# pool - `cargo test` runs the binaries SERIALLY (only threads WITHIN a█# binary). nextest also builds the test targets, so the old standalone `cargo█# build --all-targets` is dropped (clippy --all-targets below still compiles█# every target - examples/benches included - so non-test breakage is caught).█#█# TWO-PHASE split (correctness > raw speed for the release gate): the heavy█# real-broker/PTY/daemon-tree class is timing-sensitive and STARVES when it█# shares the global pool with the ~1200 light tests on a saturated self- [...1863ch elided...] _project_e2e|bind_honest_cross_perch_e2e|brain_respawn_rename|brain_split|brain_survive|daemon_refresh_e2e|dummy_harness_e2e|gateway_owner_shell_e2e|job_escape_e2e|live_adapt_translation_swap_e2e|livehost_bootgate_e2e|multi_subnet_bringup_e2e|n1_pairing|oneliner_e2e|projindex_reader_e2e|projindex_writer_e2e|resume_template_e2e|run_no_...█# nextest does not run doctests - keep them on `cargo test --doc`.█- name: Doctests█run: cargo test --workspace --doc█- name: Clippy (deny warnings)█run: cargo clippy --workspace --all-targets -- -D warnings█# ---- M5-D8a real-shell dogfood E2E (thin cross-repo hook) ----█...█# (only spt-core + the release channel moved orgs).█[Showing lines 149-183 of 529. Use :184 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading fault matrix restart rows█read(path=\"docs/FAULT-MATRIX.md\", selector=\"1-65\")█<out>█\u000e# Fault-injection acceptance matrix█**Why this exists.** ADR-0002 collapsed networking into the one per-machine daemon█(SERIOUS #6, Stage-A red team): a networking bug and a PTY bug now share a failure█domain. The collapse is locked - the owed mitigation is this matrix: inject a fault█into each subsystem **independently**, observe the blast radius (what else breaks)█and the recovery (what brings it back), and grow the proven rows until every█subsystem has injection evidence. Begun at M4-D4 (per `M4-D4-PLAN.md` piece 5);█rows land as their machinery lands.█**How to read it.** *Fault* = what is injected. *Blast radius* = the components that█degrade, by design. *Survives* = what must keep working untouched. *Recovery* = the█path back. *Evidence* = the test (or gap) backing the row. Rows marked **planned**█are accepted debt - tracked here so the matrix is the single place to look.█## Proven rows (injection evidence in-tree)█| # | Fault injected | Blast radius (by design) | Survives untouched | Recovery | Evidence |█|---|---|---|---|---|---|█| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output [...13445ch elided...] PU cost unmeasured | - | D9 + REQ-SEC-1 outer gate |█| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |█| P8 | **Receiver crash between a live-TCP WAN delivery and its op-id claim** | That one in-flight op re-delivers on replay (at-least-once window - chosen over claim-first, whose crash gap silently *drops*; see `wan.rs` module docs) | Spool path (claim+row atomic); all other ops | live-listener durable receipt, if the window ever bites |█**Maintenance rule:** when a planned row gains a real injection test, move it up with█its evidence path. When new subsystems land (D5 WAN delivery, D6 Psyche sync), add█their fault rows *in the same change* - a subsystem without a row here is a review█finding.█\u000f█</out>█# Assistant ¶█_**Inspecting process-level supervisors and controllers**_█# Tool call ¶█//Reading resume control E2E█read(path=\"crates/spt/tests/resume_no_control_steal_e2e.rs\", selector=\"1-520\")█<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 / SAME-LOCAL-IDENTITY slots:█//! `resolve_subscribe` (broker.rs) stall-evicts first, then [...22755ch elided...] sessions[i].0,█t1[i],█);█}█// Assertion 3 (no-regression guard): the trial still PROMOTES - a respawn under N live█// controllers must not break REQ-UPDATE-TRIAL-DRAIN-DRIVE. With no controller wedged,█// old_gen_drained() is true and the ready candidate promotes.█// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]█assert!(█promoted,█\"the trial must still PROMOTE the candidate under N live sessions (no regression of \\█REQ-UPDATE-TRIAL-DRAIN-DRIVE): with the Viewer re-attach no controller is wedged, so \\█old_gen_drained() is true and the ready candidate promotes. rollbacks={rollbacks:?}\"█);█assert_eq!(█promotions.as_slice(),█&[9],█\"promoted the candidate version exactly once\"█);█}█\u000f█</out>█# Tool call ¶█//Reading trial drain E2E█read(path=\"crates/spt/tests/trial_drain_drive_e2e.rs\", selector=\"1-520\")█<out>█\u000e//! UPDATE-WEDGE counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) - the A' int rig█//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running█//! production `run_brain` self-drives the broker's controller-liveness reap so a█//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand█//! the promotion DRAINED gate. This is the process-level upgrade of the in-process█//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the█//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL█//! operator take; here the candidate is the real subprocess whose OWN heartbeat█//! `Brain::sessions()` drives the reap - the exact production line the 2026-07-09█//! field freeze proved missing.█//!█//! ## The field freeze this locks down (root, log-anchored)█//!█//! `spt update fetch --apply` (v0.30.0 -> v0.30.2) hard-killed the outgoing brain;█//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the█//! broker-side writer rather than EOFing) -> `any_local_controller_wedged()` read█//! true -> the new candidate's promotion DRAINED gate (`run_trial` ->█//! `env.old_gen_drained()`) stayed false for th [...23973ch elided...] oted} promotions={promotions:?} \\█rollbacks={rollbacks:?} stall_evicts={evicts} \\█(GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"█);█// Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen█// wedge -> old_gen_drained flipped -> the latched ready promoted within the window.█// [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]█assert!(█promoted,█\"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\█must reap the black-holed old-gen controller so old_gen_drained() flips within \\█the window. RED (comment the production `let _ = brain.sessions();` in \\█...█rollbacks={rollbacks:?}\"█);█...█}█[Showing lines 1-523 of 548. Use :524 to continue]\u000f█</out>█# Tool call ¶█//Finding multiplexer test helpers█grep(pattern=\"spawn_rc|rc_sees|Command::new\\\\(.*spt|args\\\\(\\\\[\\\"rc\\\"|stdin\\\\.take|stdout\\\\.take|write_all\", path=\"crates/spt/tests\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt/tests/█## active_only_never_relay_e2e.rs█35|fn send_bounded(spt_bin: &std::path::Path, home: &std::path::Path, args: &[&str], body: &str) -> Output {█*36| let mut child = Command::new(spt_bin)█37| .no_window()█38| .args(args)█39| .env(\"SPT_HOME\", home)█...█48| .unwrap()█*49| .write_all(body.as_bytes())█50| .expect(\"write stdin body\");█51| let (tx, rx) = std::sync::mpsc::channel();█52| std::thread::spawn(move || {█## adapter_post_step.rs█28|fn run_update(spt_bin: &Path, home: &Path, mode: &str, seam_out: &Path) -> std::process::Output {█*29| Command::new(spt_bin)█30| .no_window()█31| .args([\"adapter\", \"update\", \"cc\"])█32| .env(\"SPT_HOME\", home)█## attach_wedge_e2e.rs█86|fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {█*87| let mut cmd = Command::new(spt_bin);█88| cmd.no_window()█89| .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])█90| .env(\"SPT_HOME\", home);█...█120|/// pipe (read -> 0/Err) and exits.█*121|fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {█*122| le [...14207ch elided...] home.path())█...█165| let run = {█*166| let mut cmd = Command::new(&spt_bin);█167| cmd.no_window()█168| .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])█169| .env(\"SPT_HOME\", home.path());█...█219| let state = {█*220| let mut cmd = Command::new(&spt_bin);█221| cmd.no_window()█222| .args([\"api\", \"state\", \"idle\", id, \"--session-id\", &session_id])█223| .env(\"SPT_HOME\", home.path());█...█259| let _ = {█*260| let mut cmd = Command::new(&spt_bin);█261| cmd.no_window().args([\"daemon\", \"stop\"]).env(\"SPT_HOME\", home.path());█262| output_bounded(cmd, Duration::from_secs(20))█263| };█Showing files 1-20 of 54. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding multi-session refresh tests█grep(pattern=\"const N|for .*sessions|RESTART|restart.request|daemon.*refresh|session.*survive|controller\", path=\"crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/trial_drain_drive_e2e.rs;crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt/tests/█## resume_no_control_steal_e2e.rs█3|//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker█*4|//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real█5|//! brain respawn - the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the█6|//! resume-steal bug needed and the single-session rig never exercised.█7|//!█...█15|//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:█*16|//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the█17|//! slot is free OR the incumbent is also local (`None == None`) - viewer-fallback fires█*18|//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None█*19|//! controller of every free/local-controlled session (incl. the operator's local `spt rc`█*20|//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains█21|//! (it hosts no PTY sessions - brainproc.rs) -> the session FREEZES the instant the brain takes█22|//! over + head-of-line-blocked the shared brain<->broker conn on the N-session replay -> every█23|//! journaled `rc` r [...15199ch elided...] &stop,█*269| &restart,█270| Duration::from_millis(50),█271| &NoTrialEnv,█272| Duration::from_secs(30),█...█300| );█*301| assert_eq!(broker.session_count(), 1, \"session survived the driver drop + resume\");█302| assert_eq!(█303| broker.session_pid(sid),█304| Some(child_pid),█...█308|█*309| // -- SWAP: flip the selected binary to B, then trigger a planned restart█310| // (what `apply` does: swap on disk, signal the brain to cycle). --█311| *selected.lock().unwrap() = fixture_b.clone();█*312| restart.request();█313|█314| // -- Generation 1: the supervisor respawned the brain PROCESS onto the█315| // SWAPPED binary. New pid, exe_hash == B != A (NEW BYTES RAN). --\u000f█</out>█# Assistant ¶█_**Identifying real-process broker harness**█**Comparing existing test harnesses for coverage gaps**_█# Tool call ¶█//Listing workspace crates█read(path=\"crates\", selector=null)█<out>█\u000e.█- spt/ 8h ago█- tests/ 1h ago█- src/ 2h ago█- Cargo.toml 7.8KB 8h ago█- spt-daemon/ 22h ago█- src/ 2h ago█- tests/ 22h ago█- Cargo.toml 4.2KB 22h ago█- spt-runtime/ 23h ago█- src/ 23h ago█- manifest.schema.json 40.4KB 23h ago█- Cargo.toml 1001B 1w ago█- spt-store/ 5d ago█- src/ 2h ago█- tests/ 5d ago█- Cargo.toml 823B 5d ago█- spt-net/ 1w ago█- Cargo.toml 4.0KB 1w ago█- src/ 1w ago█- xtask/ 1w ago█- src/ 22h ago█- Cargo.toml 749B 1w ago█- spt-term/ 1w ago█- src/ 1w ago█- tests/ 1w ago█- Cargo.toml 1.4KB 1w ago█- spt-proto/ 1w ago█- src/ 1w ago█- Cargo.toml 351B 1w ago█- spt-msg/ 1w ago█- src/ 7h ago█- tests/ 1w ago█- Cargo.toml 588B 1w ago█- spt-live/ 1w ago█- src/ 5d ago█- Cargo.toml 636B 1w ago\u000f█</out>█# Tool call ¶█//Finding mock binary declaration█grep(pattern=\"name\\\\s*=\\\\s*\\\"mock-session\\\"|DUMMY_HARNESS_TICK|struct.*Dummy|heartbeat_ms\", path=\"Cargo.toml;crates\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt/█## src/█### cli.rs█4145| /// Peer-pump last-tick wall-clock ms (`None` = no heartbeat recorded).█*4146| pump_heartbeat_ms: Option<u64>,█4147| /// The managing service label, if a registered service owns this daemon.█4148| managed_by: Option<String>,█4149| managed_active: Option<bool>,█...█4318| .then(|| broker_image.as_deref() != Some(env!(\"CARGO_PKG_VERSION\")));█*4319| let pump_heartbeat_ms = if running {█4320| spt_daemon::read_pump_heartbeat()█4321| } else {█4322| None█...█4369| net_up,█*4370| pump_heartbeat_ms,█4371| managed_by,█4372| managed_active,█4373| subnets,█## tests/█### attach_wedge_e2e.rs█114|█*115|/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within█116|/// the window - the strong \"this endpoint is being SERVED\" proof. The rc child is then█117|/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) - exactly the█118|/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the█...█141| acc.push_str(&String::from_utf8_lossy(&buf [...2898ch elided...] out bytes, no heartbeat).\\n=== endpoint run stdout ===\\n{run_captured}\\█627| \\n=== endpoint run stderr ===\\n{run_stderr_txt}\\n=== brain stderr ===\\n{brain_stderr}\",█### multi_subnet_bringup_e2e.rs█405| acc.push_str(&String::from_utf8_lossy(&buf[..n]));█*406| if acc.contains(\"DUMMY_HARNESS_TICK\") {█407| let _ = tx.send(acc.clone());█408| break;█409| }█...█416| let rc_captured = rx.recv_timeout(Duration::from_secs(12)).unwrap_or_default();█*417| let rc_saw_tick = rc_captured.contains(\"DUMMY_HARNESS_TICK\");█418|█419| // Re-read status AFTER the attach: it must STILL be UNBOUND - proving the attach█420| // landed on a genuinely pre-bind session, never a bound one.\u000f█</out>█# Tool call ¶█//Reading dummy controller harness█read(path=\"crates/spt/tests/dummy_harness_e2e.rs\", selector=\"1-330\")█<out>█\u000e//! Wave 1 (v0.12.1) - the DUMMY-HARNESS fixture smoke test. The permanent█//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the█//! REAL spt-hosted bringup path end-to-end -█//! `spt endpoint run` -> `launch_harness_brokered_in` -> broker PTY -> `spt rc` attach█//! - against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and█//! NOT a mock that exits immediately.█//!█//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the█//! harness contract), prints a stdout heartbeat on an interval, and stays alive█//! until killed - so \"broker wedged / endpoint died\" is cleanly separable from \"a█//! real harness failed to launch.\" Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,█//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.█//!█//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the█//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the█//! live heartbeat output. Bounded; all spawned pids reaped scoped (never█//! machine-wide - shared runner).█use std::io::Read;█use std::path::{Path, PathBuf};█use std::process::{Child, Command, Output, Stdio};█use [...13574ch elided...] in an ATTACH terminal action█// (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In█// `cmd_endpoint_run` the flow is `launch_harness_brokered_in` (returns once the█// harness PROCESS is spawned) -> `run_attach`, with NO await-online between them.█// The broker-PTY bind (info status->STATUS_ONLINE + a live session) lands ASYNC,█// so the immediate `run_attach` loses the race: it either short-circuits on the█// offline status-gate (\"offline - nothing to attach\", REQ-HAZARD-RC-ATTACH-FAILFAST█// in rc.rs run_attach) or hits \"no live session for endpoint\" (printed as█// RC_FAIL) - never connecting to the live PTY, so NO heartbeat is ever seen.█//█// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live█[Showing lines 1-333 of 631. Use :334 to continue]\u000f█</out>█# Tool call ¶█//Finding refresh test limitations█grep(pattern=\"does NOT|not assert|limitation|single|one hosted|alive|functional|traffic|bidirectional\", path=\"crates/spt/tests/daemon_refresh_e2e.rs;crates/spt/tests/brain_survive.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt/tests/█## daemon_refresh_e2e.rs█200|█*201| // The hosted endpoint survives: harness alive + perch back/still ONLINE.█*202| let harness_alive_after = harness_pid█*203| .map(spt_store::proc::is_process_alive)█204| .unwrap_or(false);█205| let mut still_online = false;█206| let online_deadline = Instant::now() + Duration::from_secs(10);█...█218| refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\█*219| broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\█220| still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\█221| --- refresh stderr ---\\n{refresh_stderr}\\n=== endpoint run stderr ===\\n{run_stderr}\\n\\█222| === brain stderr ===\\n{brain_stderr}\",█...█286| assert!(█*287| harness_alive_after,█*288| \"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\█289| refresh - only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\█290| === brain stderr ===\\n{brain_stderr}\"█291| );█## brain_survive.rs█9|//!█*10|//! No single test proves the whole cl [...4831ch elided...] 1█79|//!█80|//! The SOLE `#[test]` here by design: it owns the process-global `SPT_HOME` (the subprocess█81|//! brain derives `broker_socket_name()` from it - the brain_survive.rs / trial_drain_drive█## attach_wedge_e2e.rs█21|//! residual loopback write. This test is the faithful real-daemon proof that the wedge█*22|//! does not occur - and folds in L3 (dead endpoint offlined within one█23|//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).█24|//!█25|//! All spawned pids are reaped SCOPED (never machine-wide - shared CI runner).█...█127| .stderr(Stdio::null())█*128| .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early█129| .spawn()█130| .expect(\"spawn spt rc <id>\");█131| let _rc_stdin = rc.stdin.take();\u000f█</out>█# Tool call ¶█//Reading test scheduling config█read(path=\".config/nextest.toml\", selector=\"1-180\")█<out>█\u000e# cargo-nextest configuration.█#█# CI runs the workspace test suite under `cargo nextest run` in TWO PHASES (see the█# `test` job in .github/workflows/ci.yml):█# Phase A `-E 'not ( <HEAVY> )'` - the light pool, full parallelism, fast.█# Phase B `-E '<HEAVY>'` - the heavy real-broker/PTY/daemon-tree class,█# run AFTER Phase A so the box is QUIET, and█# serialized by the test-group below.█# <HEAVY> is the SAME filter string in both CI phases AND in the test-group override█# at the bottom of this file - keep all three in sync (a clean partition: A is the█# exact negation of B, so nothing is dropped or double-run).█# See docs/RELEASE-RUNBOOK + ci-kitsubito.█[profile.default]█# A hung test (e.g. a blocking PTY/socket read that never completes) must FAIL FAST,█# never wedge the CI job up to its wall-clock timeout. Warn once a test exceeds 60s,█# then SIGKILL + fail it after 4 such periods (4 minutes). No legitimate test in this█# workspace runs that long; this is purely the no-infinite-wedge backstop.█slow-timeout = { period = \"60s\", terminate-after = 4 }█# Concurrency cap for the heavy real-broke [...5482ch elided...] LIGHT█# Phase-A pool at full parallelism. `rc::tests::attach_viewport_reconnects_across_a_broker_bounce`█# (a real broker + a broker-bounce reconnect) then TIMED OUT @240s under a 360-way█# `-p spt --bin spt` run; 1/1 ISOLATED green in ~1.3s - the 4th distinct full-parallel█# trip, so the remedy is RECIPE hardening (serialize the class), not the test. Fold the█# rc broker/bounce class into the same heavy group so it can never be starved again;█# EXTEND this name set on each new sighting (the FLAKE-LEDGER discipline).█[[profile.default.overrides]]█filter = 'package(spt) & kind(bin) & test(/^rc::tests::(attach_viewport_reconnects_across_a_broker_bounce|broker_host_child|reconnect_class_only_severed_transport_redrives)$/)'█test-group = 'heavy-broker-pty'█[Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Finding real update apply PTY tests█grep(pattern=\"update.*apply|apply.*update|spt update|apply_staged|request_brain_restart|restart.request\", path=\"crates/spt/tests;crates/spt-daemon/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt/tests/█### bootstrap_verb_e2e.rs█5|//! at the canonical `<SPT_HOME>/bin` path, is idempotent on re-run, and the█*6|//! PLACED binary's `spt update fetch` speaks the gh channel - proven by the█7|//! loud `GhCliRequired` refusal when no `gh` is resolvable, which is the gh█8|//! carrier gate itself, not an HTTPS fallback.█9|//!█...█86| .output()█*87| .expect(\"run placed spt update fetch\");█88| let stderr = String::from_utf8_lossy(&fetch.stderr);█89| assert!(!fetch.status.success(), \"fetch without gh must refuse\");█90| assert!(█### brain_respawn_rename.rs█122| // P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly█*123| // what `spt update apply` does (rename-then-replace). --█124| let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));█125| std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");█126| std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");█### brain_survive.rs█311| *selected.lock().unwrap() = fixture_b.clone();█*312| restart.request();█313|█314| // -- Generation 1: the supervisor respawned the brain PROCESS onto the█3 [...7422ch elided...] tall-evicted),█### propagate.rs█723|█*724| // Round 3: the operator ran `spt update apply` - converged.█725| lab.cache.record_applied(target).unwrap();█726| lab.cache.record_last_outcome(\"applied\").unwrap();█727| let (report, _) = status_pull(&coord, &lab, &trust);█### replicate.rs█30|};█*31|use spt_net::net::replicate::{apply_update, LineDecoder, RegistryUpdate};█32|use spt_proto::identity::Identity;█33|use spt_store::epoch::EpochSource;█34|use spt_store::visibility::VisibilityStore;█...█222| for upd in decoder.push(&bytes) {█*223| outcomes.push(apply_update(&mut b_regs, upd).expect(\"member subnet\"));█224| }█225| }█226| BrokerEvent::NetStreamEof { stream_id, .. } if stream_id == stream => break,\u000f█</out>█# Assistant ¶█_**Planning workspace crate search**_█# Tool call ¶█//Finding mock adapter package█grep(pattern=\"mock-adapter|mock-session|DUMMY_HARNESS_TICK\", path=\".\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# CONTEXT.md█863|**repo topology (The Forkening, ADR-0036)**:█*864|Development home is **`BigscreenVR/spt-bs-core`** (private); the publish target is **`BigscreenVR/spt-bs-releases`** (private) - GitHub Releases carry the binaries, the docs bundle, and the signed update-set, reached only through an org-authenticated `gh`. **`SaberMage/spt-core` and `SaberMage/spt-releases` are retained as passive mirrors** (private; the merging agent pushes every main merge + tags; Actions disabled) - full history, may diverge from the dev home only if a genuine second contributor e...█865|_Avoid_: treating a releases repo as a second source repo; authoring doc truth there; publishing anything to the mirrors first.█866|█867|**issue / feature tracking**:█# Cargo.lock█2341|[[package]]█*2342|name = \"mock-adapter\"█2343|version = \"0.33.0\"█2344|dependencies = [█2345| \"spt-runtime\",█# M2b-PLAN.md█61|stale-signoff sweep); the **resume-session seam** (fresh-with-preload +█*62|continue-existing); the `api listen` wiring that hosts all of it; mock-adapter█63|lifecycle templates + a LiveAgent E2E.█64|█65|**Out:**█# M5-D4-PLAN.md█201| slice over an in-flight run.█*202|- `cargo build -p mock-ada [...16375ch elided...] bind_cwd_project_e2e.rs█109| .unwrap_or_default();█*110| let mock_session = sibling_bin(\"mock-session\");█111| assert!(█112| mock_session.exists(),█113| \"the dummy-harness program must be built: {} \\█*114| (pre-build: cargo build -p mock-adapter --bin mock-session)\",█115| mock_session.display()█116| );█117|█...█125| \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\█*126| --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"█127| );█128| let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");█129| std::fs::create_dir_all(&src).unwrap();█Showing files 1-20 of 41. Use skip=20 for the next page, or narrow paths/pattern.█[Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Reading refresh implementation seam█read(path=\"crates/spt-daemon/src/applyhost.rs\", selector=\"320-375,780-855\")█<out>█\u000epub fn apply_staged(█...█) -> Result<ApplyStagedOutcome, String> {█...█// brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not█// attach to any session (the broker holds every hosted endpoint across the█// brain cycle - that is the whole point).█let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;█swap_and_record(cache, exe_path, version, &artifact)?;█// REQ-UPD-3's moment (ADR-0018 D3-3): trigger a real brain-PROCESS restart.█// The broker signals its supervisor to cycle the brain child onto the█// just-swapped binary with `start-reason=update`; new code runs immediately,█// no manual bounce, every broker-held endpoint untouched at the process█// level. This replaces the old in-process `apply_brain_only` handoff (the█// regression: it re-attached a CLI-side subscriber while the live brain kept█// running the OLD code). `classify` already gated this as brain-only, so the█// broker never moves; only the brain restarts.█//█// A broker without a supervisor wired (`honored: false`) still swapped the█// binary - the next daemon start uses the new executable. Readiness-gat [...4769ch elided...] \"no supervisor wired must report honored=false\"█);█// Supervisor wired: refresh raises the same planned-restart signal the█// apply path rides - with nothing staged anywhere.█let signal = Arc::new(crate::brainproc::BrainRestart::new());█assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");█assert!(█refresh_brain(&name).expect(\"refresh round-trips\"),█\"a wired supervisor must report honored=true\"█);█assert!(█signal.take(),█\"refresh must raise the planned-restart signal the supervisor consumes\"█);█}█fn echo_spawn_req() -> SpawnReq {█#[cfg(unix)]█let (program, args) = (\"cat\".to_string(), Vec::<String>::new());█...█}\u000f█</out>█# Tool call ¶█//Reading brain boot resume seam█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"170-295\")█<out>█\u000e // generation is broker-owned custody of the KH-2.4 counter (no longer the█// brain->brain BrainState.generation, which retires in D4), and the reason is█// Q4's update-vs-crash discriminator that D5 will use to decide whether to█// preserve or reset phase-significant loop timing. D3-2 surfaces them; D5█// consumes the reason. `gen_start` stays fresh-now per KH-2.4 - only the█// *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 stil [...6164ch elided...] // 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█// net_status probe above already owns broker-liveness). Self-applies: it drives█// the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands█// its own trial against a live older broker - no coordinated broker restart.█// [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]█let _ = brain.sessions();█write_ready(generation);█}█[Showing lines 169-298 of 2116. Use :299 to continue]\u000f█</out>█# Tool call ¶█//Reading controller attach semantics█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"5450-5570\")█<out>█\u000e█/// A connected local-socket pair for the unit: bind a listener\u000e, connect a█/// client `Stream`, accept the server `Stream`. Returns the SERVER send half█/// wrapped as a `SharedSend` (the broker side a `become_controller` writes█/// through) plus the CLIENT `Stream` (the brain side we read the wire off) and█/// the unused server RECV half (held so the server fd stays open while we█/// read). Mirrors `LocalSocketTransport`'s own round-trip test + `handle_conn`'s█/// `conn.split()`.█fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {█use std::sync::atomic::AtomicU32;█// A process-unique pipe/UDS name so parallel test threads never collide.█static SEQ: AtomicU32 = AtomicU32::new(0);█let name = format!(█\"spt-daemon-p1c-{}-{}.sock\",█std::process::id(),█SEQ.fetch_add(1, Ordering::Relaxed)█);█let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");█let client = LocalSocketTransport::connect(&name).expect(\"connect client\");█let server = listener.accept().expect(\"accept server side\");█let (recv, [...4819ch elided...] end), None, 0);█// Release the socket: the writers race for the mutex. The in-lock gate makes█// superseded writer-A bail; only writer-B's current-screen repaint@2 lands.█drop(guard);█let seqs = drain_output_seqs(&mut client, 1);█assert_eq!(█seqs,█vec![2],█\"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\█wire - a superseded writer-A flushing its stale repaint@1 would put seq \\█1 first (the reorder this hazard guards)\"█);█// No second frame leaks: writer-A is epoch-gated silent, writer-B parks on█// its (empty) live channel. A bounded read returns None (not a stale frame).█...█}█[Showing lines 5449-5573 of 6009. Use :5574 to continue]\u000f█</out>█# Tool call ¶█//Reading update ADR rules█read(path=\"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md\", selector=\"1-90\")█<out>█\u000e# Single consolidated daemon with broker/brain split; peer-propagated gated self-update█## Status█accepted (2026-05-29)█## Context█ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:█1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.█2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* - we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).█## Decision█**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` - [...8317ch elided...] t (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)█**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) -> the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).█\u000f█</out>█# Assistant ¶█_**Planning specific cargo test commands**█**Inspecting mock-session stdin handling**_█# Tool call ¶█//Reading dummy harness behavior█read(path=\"adapters/mock/src/main.rs\", selector=\"1-150\")█<out>█\u000e//! `mock-session` - the generic mock adapter's \"agent session\". A trivial,█//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,█//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's█//! `[session.self]` template, it does what a real session's boot hook would do:█//! fire `spt api bind` to flip its perch live, then `spt api state idle`.█//!█//! It shells out to the `spt` binary exactly as a real adapter would - resolved█//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves█//! the inbound contract is harness-agnostic: a dumb helper drives it with only█//! `spt api` calls.█use std::process::Command;█fn arg(args: &[String], flag: &str) -> Option<String> {█args.iter()█.position(|a| a == flag)█.and_then(|i| args.get(i + 1).cloned())█}█fn main() {█let args: Vec<String> = std::env::args().skip(1).collect();█// M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core█// binds its nested perch; the child just needs to launch). `echo` is the█// cheap-model echo-commune: read history on stdin, emit a brief on stdout.█match arg(&args, \"--mode\").as_deref() [...5895ch elided...] ln!(\"mock-session: --id required\");█std::process::exit(2);█});█let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());█let spt = arg(&args, \"--spt\")█.or_else(|| std::env::var(\"SPT_BIN\").ok())█.unwrap_or_else(|| \"spt\".to_string());█// Optional endpoint type (the open type system): a non-agent endpoint - e.g.█// a Gateway - binds with `--type gateway`. Absent => the default live_agent.█let endpoint_type = arg(&args, \"--type\");█let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());█// 1. Post-spawn bind: flip the perch skeleton->live under this session id.█let mut bind_args = vec![█\"api\".to_string(),█...█];█...█}█[Showing lines 1-153 of 191. Use :154 to continue]\u000f█</out>█# Tool call ¶█//Reading rc data path tests█read(path=\"crates/spt/src/rc.rs\", selector=\"1-180\")█<out>█\u000e//! `spt rc <id>` - the operator-side attach pump (M12-W1, REQ-RC-1): attach a█//! local terminal to a broker-held PTY session.█//!█//! **One pump, swappable transport** (the doyle invariant). The attach protocol█//! is `serve_attach` on the target side ⇄ this pump on the operator side, both█//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the█//! degenerate single-node case: the operator dials its OWN node (loopback peer)█//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`█//! against the same broker - the exact cross-node accept+serve path, no local█//! special-case branch. The one-path invariant lives at this pump, not the█//! transport under the stream.█//!█//! **Detach never kills the session.** The broker owns the PTY; closing our send█//! side ends the viewport with the session still running (`serve_attach` ->█//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS█//! attach-lifecycle invariant).█//!█//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread█//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel█//! (and watches for the detach keybind); the [...6356ch elided...] j] - b'0') as u32;█cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));█}█b';' => {█params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);█cur = None;█}█b'h' | b'l' => {█if let Some(v) = cur {█params.push(v.min(u16::MAX as u32) as u16);█}█return DecsetParse::Complete {█consumed: j + 1,█params,█set: s[j] == b'h',█};█}█// Any other byte => this isn't a clean private-mode set/reset.█_ => return DecsetParse::NotDecset,█}█j += 1;█}█...█}█[Showing lines 1-183 of 3214. Use :184 to continue]\u000f█</out>█# Tool call ¶█//Finding rc stdin pump symbols█grep(pattern=\"spawn_stdin|stdin.*reader|send_attach_input|stdin_rx|read\\\\(&mut|write_all|detach\", path=\"crates/spt/src/rc.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 12|//!█*13|//! **Detach never kills the session.** The broker owns the PTY; closing our send█14|//! side ends the viewport with the session still running (`serve_attach` ->█*15|//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS█16|//! attach-lifecycle invariant).█17|//!█18|//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread█*19|//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel█*20|//! (and watches for the detach keybind); the main loop interleaves draining that█*21|//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.█22|// [impl->REQ-RC-1]█23|█24|use std::io::{Read, Write};█...█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;█...█34|█*35|/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the█*36|/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one█37|/// literal ctrl-b to the session; any other byte after the prefix is [...24882ch elided...] er.feed(plain) {█*3185| sink.write_all(&status.assert_bytes()).unwrap();█3186| }█3187| assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");█3188|█...█3196| fired = true;█*3197| sink.write_all(&status.assert_bytes()).unwrap();█3198| }█3199| assert!(fired, \"the alt-screen enter trips the re-assert scanner\");█3200| assert!(sink.len() > before, \"a re-assert was appended after the trigger\");█...█3206| // (c) pump EXIT: restore (reset region + clear row 1).█*3207| sink.write_all(&StatusRow::restore_bytes()).unwrap();█3208| assert!(█3209| sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),█3210| \"exit restores the full scroll region and clears the reserved row\"\u000f█</out>█# Tool call ¶█//Reading physical conn regression gate█read(path=\"crates/spt-daemon/tests/brain_decouple.rs\", selector=\"430-835\")█<out>█\u000efn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {█SpawnReq {█...█args: vec![█...█.to_string(),█],█rows: 24,█cols: 80,█endpoint: endpoint.to_string(),█cwd: None,█env: Default::default(),█translation_binary: None,█adapter: String::new(),█install_dir: None,█}█}█/// One `KIND_SESSIONS` round-trip, returning the full reply. The dispatch runs█/// `reap_dead_controller` -> the time-based `stall_evict_controller`, so polling█/// this both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.█/// Bounded thread + channel (a wedge surfaces as a timeout, not a hang), like the█/// existing [`stall_evict_count`] helper. The transient conn is dropped when the█/// thread returns.█#[cfg(windows)]█fn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {█let name = name.to_string();█let (tx, rx) = std::sync::mpsc::channel();█thread::spawn(move || {█let mut c = connect(&name);█send(&mut c, spt_daemon::msg::KIND_SESSIONS, serde_json::Value::Null);█loop {█match read_frame(&mut c) {█Ok( [...16433ch elided...] old_client_write_is_err,█Some(true),█\"a write on the OLD client connection must FAIL once the physical conn is \\█retired. PRE-FIX RED: the conn is still alive so the write succeeds (Ok).\"█);█// The fresh controller resumes from the frozen cursor with NO lost frame.█assert!(█matches!(█s9_outcome,█Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)█),█\"a fresh controller from the frozen cursor must take control; got {s9_outcome:?}\"█);█assert_eq!(█s9_first_seq,█Some(resume_seq),█\"the fresh controller's FIRST replayed frame seq must equal the frozen resume \\█cursor (no lost frame); got first_seq={s9_first_seq:?} resume_seq={resume_seq}\"█);█}█\u000f█</out>█# Tool call ¶█//Reading blackhole lifecycle gate█read(path=\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\", selector=\"1-330,560-650\")█<out>█\u000e//! MSG-IDENTITY W6 / F-039 leg (e) - the DETERMINISTIC BLACK-HOLED-CONTROLLER█//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker█//! hosting REAL PTY children over the real local IPC.█//!█//! ## What this is█//!█//! This is hertz's RCA fix-shape item 5 made executable (`.claude/█//! hertz-pty-freeze-rca.md` §Proposed fix shape, item 5): the standing█//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the█//! EXACT Windows named-pipe black-hole (a controller that stops draining while a█//! finite burst floods its socket, so the broker's writer parks inside█//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` - no supported█//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/█//! src/conn.rs` bounded/cancelable/poison-on-failure writes, landed fc048bb),█//! asserts hertz's FIVE invariants for ONE incident:█//!█//! 1. UNRELATED SESSIONS CONTINUE - a second, independent session's viewer keeps█//! receiving output throughout the black-hole incident (the fan-out never█//! blocks the whole broker on the one wedged conn);█//! 2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE B [...17132ch elided...] INVARIANT 1: the UNRELATED session's viewer must keep receiving output \\█ACROSS the black-hole incident - the fan-out must never block the whole \\█broker on the one wedged conn; got u_baseline={u_baseline} u_final={u_final}\"█);█// -- INVARIANT 2: THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE█// BOUND - a write on the OLD client conn FAILS (it was physically retired,█// not merely logically stall-evicted). --█assert_eq!(█old_client_write_is_err,█Some(true),█\"INVARIANT 2: a write on the OLD black-holed client connection must FAIL - \\█the physical conn must be canceled/closed within the write bound of logical \\█release. A success means the conn is still physically alive (the r4 RED).\"█);█...█}\u000f█</out>","textHead":"# User ¶█Complete the assignment below, thoroughly:█# Target█Existing integration/e2e tests and historical hazard documentation covering daemon refresh with attached PTYs. Non-goal: source-wide architecture summary.█# Change█Locate the closest real-process test harness that can start broker/brain, attach multiple controllers, perform daemon refresh or brain replacement, and assert bidirectional PTY traffic afterward. Identify how to extend or compose it into a deterministic feedback loop without touching live user sessions.█# Acceptance█Return exact tests/helpers/commands, limitations, and the smallest deterministic pass/fail scenario matching the reported symptom. No edits or live refreshes.█# Assistant ¶█_**Planning investigation of diagnose skill and globbing tests**_█# Tool call ¶█//Reading 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 required documents█glob(path=\"CONTEXT.md;docs/KNOWN-HAZARDS.md;docs/DEBUG-ROLLOUT.md;docs/adr/**/*;docs/ADR*;**/*refresh*;**/*integration*;**/*e2e*\", hidden=true, gitignore=true, limit=300)█<out>█\u000eCONTEXT.md█# docs-site/src/harness-contract/█integration-checklist.md█# crates/█## spt/tests/█projindex_reader_e2e.rs█dummy_harness_e2e.rs█projindex_writer_e2e.rs█whoami_identity_e2e.rs█daemon_refresh_e2e.rs█composite_e2e.rs█docs_bundle_e2e.rs█release_verify_e2e.rs█bootstrap_verb_e2e.rs█trial_drain_drive_e2e.rs█resume_no_control_steal_e2e.rs█send_stamp_agent_id_e2e.rs█endpoint_autostart_e2e.rs█list_json_liveness_parity_e2e.rs█listen_seed_retry_e2e.rs█active_only_never_relay_e2e.rs█attach_wedge_e2e.rs█worker_visibility_e2e.rs█worker_lifecycle_e2e.rs█psyche_download_e2e.rs█notify_shell_e2e.rs█multi_subnet_bringup_e2e.rs█live_adapt_translation_swap_e2e.rs█tunnel_e2e.rs█shell_sleepwake_e2e.rs█shell_e2e.rs█shell_actgate_e2e.rs█run_no_dup_session_e2e.rs█resume_template_e2e.rs█ready_resume_ledger_e2e.rs█quickstart_e2e.rs█psyche_sid_custody_e2e.rs█poll_envelope_e2e.rs█oneliner_e2e.rs█nested_resolution_e2e.rs█live_resolve_e2e.rs█live_firsthost_e2e.rs█live_bind_firsthost_e2e.rs█livehost_bootgate_e2e.rs█job_escape_e2e.rs█idle_edge_drain_e2e.rs█gateway_owner_shell_e2e.rs█gateway_e2e.rs█drive_e2e.rs█contract_e2e.rs█boundary_ready_strand_e2e.rs█bind_honest_cross_perch_e2e.rs█bind_cwd_project_e2e [...1150ch elided...] 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 refresh tests█grep(pattern=\"daemon refresh|brain replacement|replace.*brain|refresh.*daemon|DaemonRefresh|refreshDaemon\", path=\"tests;test;src;docs;crates;packages\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# docs/█## KNOWN-HAZARDS.md█620|- **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 e...█*621|- **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 ...█622|- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 mult [...8144ch elided...] cmd = Command::new(&spt_bin);█186| cmd.no_window()█...█216| eprintln!(█*217| \"=== W4 daemon refresh: run_ok={} online={online} harness_pid={harness_pid:?} \\█218| refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\█219| broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\█220| still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\█...█260| refresh.status.success(),█*261| \"REQ-DAEMON-REFRESH: `spt daemon refresh` must exit 0 against a live daemon \\█262| (stdout: {refresh_stdout}; stderr: {refresh_stderr}).\\n\\█263| === brain stderr ===\\n{brain_stderr}\"█264| );█Skipped missing paths: test, packages█[Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Finding PTY harnesses█grep(pattern=\"broker|brain|controller|attach|pty|bidirectional|real.process|child_process|spawn\", path=\"tests;test;src;docs;crates;packages\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# docs/█## BROKER-BRAIN-SPLIT-RESTORATION.md█*1|# Broker/Brain Split Restoration - Design Rationale (pre-ADR)█2|█*3|**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` - see §0); six amendments folded in (marked **[V1]**-**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 - restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...█4|█5|**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.█6|█...█12|█*13|**Verdict: the core holds.** Confirmed at source - broker is an in-process thread (no broker child-spawn exists anywhere in the repo); no doc/plan/commit marks the collapse deliberate (silent drift; regression framing correct); `update.rs:233-234`'s \"exec the new binary's brain\" is aspirational and never wired; `applyhost.rs:176` records `applied` before the handoff (the optimistic `applied.json` watched on `enlyzeam`); `applyhost.rs:238-239` re- [...48504ch elided...] || 2 | `spt-term digest_parses_a_real_pty_stream` | 3× gravity (2026-06-03; 2026-06-04 runs 26957386850, 26966738231) | sprint-collapse assert (`digest.rs`) - PTY input echo and the child's write are **concurrent writers** into the master-read stream; back-to-back `send_line`s let a later line's echo interleave mid-`Write(...)`, breaking the tool regex for that turn | HARDENED (2026-06-04): sends serialized - each line pumped back (adaptive echo-copy count, 2s/line bound) before the next, so no concu...█12|| 3 | `spt-daemon sync.rs torn_pull` + `concurrent_writes` | 1× (2026-06-03) | `wait_for_stream` deadline too tight under load | HARDENED: `wait_for_stream_except` 2s->10s; no recurrence |█[Showing lines 1-336 of 706 (49.6KB limit). Use :337 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Assistant ¶█_**Planning targeted test module discovery**_█# Tool call ¶█//Reading domain ownership rules█read(path=\"CONTEXT.md\", selector=\"1-220\")█<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**: [...45094ch 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-223 of 883. Use :224 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading refresh E2E█read(path=\"crates/spt/tests/daemon_refresh_e2e.rs\", selector=\"1-340\")█<out>█\u000e//! THE-FORKENING W4 - `spt daemon refresh` on a LIVE daemon with a hosted█//! endpoint (REQ-DAEMON-REFRESH): the brain generation cycles (new brain pid,█//! generation bumped) with NO binary swap, while the broker process, the hosted█//! harness session, and the endpoint's ONLINE perch all survive untouched - the█//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI█//! verb. The recovery story: wedged brain-held state no longer needs a full█//! daemon bounce that kills every PTY.█//!█//! Real `spt daemon run` broker + supervised brain + a REAL dummy-harness█//! `[session.self]` session (the live_adapt_translation_swap_e2e rig, minus the█//! translation child). Bounded everywhere; every spawned pid is reaped SCOPED█//! at test end.█//█// [int->REQ-DAEMON-REFRESH]█use std::path::{Path, PathBuf};█use std::process::{Child, Command, Output, Stdio};█use std::time::{Duration, Instant};█mod common;█use common::CommandNoWindowExt;█use spt_store::perch::{self, ParentHint};█/// Scoped pid-tree kill (cleanup) - never machine-wide.█fn kill_pid(pid: u32) {█#[cfg(windows)]█let _ = Command::new(\"taskkill\")█.no_window()█.args([\"/PID\", &pid.to_st [...10099ch elided...] }) - the supervisor cycle, not a re-stamp.\\n\\█=== brain stderr ===\\n{brain_stderr}\"█);█assert!(█broker_still_running,█\"REQ-DAEMON-REFRESH: the broker process must survive the refresh untouched \\█(pid {broker_pid}).\\n=== brain stderr ===\\n{brain_stderr}\"█);█assert!(█harness_alive_after,█\"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\█refresh - only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\█=== brain stderr ===\\n{brain_stderr}\"█);█assert!(█still_online,█\"REQ-DAEMON-REFRESH: the endpoint perch must remain ONLINE across the refresh \\█(the session was never torn down).\\n=== brain stderr ===\\n{brain_stderr}\"█);█}█\u000f█</out>█# Tool call ¶█//Reading broker hazard history█read(path=\"docs/KNOWN-HAZARDS.md\", selector=\"624-710\")█<ou","textTail":"\u000e, connect a█/// client `Stream`, accept the server `Stream`. Returns the SERVER send half█/// wrapped as a `SharedSend` (the broker side a `become_controller` writes█/// through) plus the CLIENT `Stream` (the brain side we read the wire off) and█/// the unused server RECV half (held so the server fd stays open while we█/// read). Mirrors `LocalSocketTransport`'s own round-trip test + `handle_conn`'s█/// `conn.split()`.█fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {█use std::sync::atomic::AtomicU32;█// A process-unique pipe/UDS name so parallel test threads never collide.█static SEQ: AtomicU32 = AtomicU32::new(0);█let name = format!(█\"spt-daemon-p1c-{}-{}.sock\",█std::process::id(),█SEQ.fetch_add(1, Ordering::Relaxed)█);█let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");█let client = LocalSocketTransport::connect(&name).expect(\"connect client\");█let server = listener.accept().expect(\"accept server side\");█let (recv, [...4819ch elided...] end), None, 0);█// Release the socket: the writers race for the mutex. The in-lock gate makes█// superseded writer-A bail; only writer-B's current-screen repaint@2 lands.█drop(guard);█let seqs = drain_output_seqs(&mut client, 1);█assert_eq!(█seqs,█vec![2],█\"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\█wire - a superseded writer-A flushing its stale repaint@1 would put seq \\█1 first (the reorder this hazard guards)\"█);█// No second frame leaks: writer-A is epoch-gated silent, writer-B parks on█// its (empty) live channel. A bounded read returns None (not a stale frame).█...█}█[Showing lines 5449-5573 of 6009. Use :5574 to continue]\u000f█</out>█# Tool call ¶█//Reading update ADR rules█read(path=\"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md\", selector=\"1-90\")█<out>█\u000e# Single consolidated daemon with broker/brain split; peer-propagated gated self-update█## Status█accepted (2026-05-29)█## Context█ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:█1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.█2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* - we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).█## Decision█**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` - [...8317ch elided...] t (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)█**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) -> the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).█\u000f█</out>█# Assistant ¶█_**Planning specific cargo test commands**█**Inspecting mock-session stdin handling**_█# Tool call ¶█//Reading dummy harness behavior█read(path=\"adapters/mock/src/main.rs\", selector=\"1-150\")█<out>█\u000e//! `mock-session` - the generic mock adapter's \"agent session\". A trivial,█//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,█//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's█//! `[session.self]` template, it does what a real session's boot hook would do:█//! fire `spt api bind` to flip its perch live, then `spt api state idle`.█//!█//! It shells out to the `spt` binary exactly as a real adapter would - resolved█//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves█//! the inbound contract is harness-agnostic: a dumb helper drives it with only█//! `spt api` calls.█use std::process::Command;█fn arg(args: &[String], flag: &str) -> Option<String> {█args.iter()█.position(|a| a == flag)█.and_then(|i| args.get(i + 1).cloned())█}█fn main() {█let args: Vec<String> = std::env::args().skip(1).collect();█// M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core█// binds its nested perch; the child just needs to launch). `echo` is the█// cheap-model echo-commune: read history on stdin, emit a brief on stdout.█match arg(&args, \"--mode\").as_deref() [...5895ch elided...] ln!(\"mock-session: --id required\");█std::process::exit(2);█});█let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());█let spt = arg(&args, \"--spt\")█.or_else(|| std::env::var(\"SPT_BIN\").ok())█.unwrap_or_else(|| \"spt\".to_string());█// Optional endpoint type (the open type system): a non-agent endpoint - e.g.█// a Gateway - binds with `--type gateway`. Absent => the default live_agent.█let endpoint_type = arg(&args, \"--type\");█let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());█// 1. Post-spawn bind: flip the perch skeleton->live under this session id.█let mut bind_args = vec![█\"api\".to_string(),█...█];█...█}█[Showing lines 1-153 of 191. Use :154 to continue]\u000f█</out>█# Tool call ¶█//Reading rc data path tests█read(path=\"crates/spt/src/rc.rs\", selector=\"1-180\")█<out>█\u000e//! `spt rc <id>` - the operator-side attach pump (M12-W1, REQ-RC-1): attach a█//! local terminal to a broker-held PTY session.█//!█//! **One pump, swappable transport** (the doyle invariant). The attach protocol█//! is `serve_attach` on the target side ⇄ this pump on the operator side, both█//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the█//! degenerate single-node case: the operator dials its OWN node (loopback peer)█//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`█//! against the same broker - the exact cross-node accept+serve path, no local█//! special-case branch. The one-path invariant lives at this pump, not the█//! transport under the stream.█//!█//! **Detach never kills the session.** The broker owns the PTY; closing our send█//! side ends the viewport with the session still running (`serve_attach` ->█//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS█//! attach-lifecycle invariant).█//!█//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread█//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel█//! (and watches for the detach keybind); the [...6356ch elided...] j] - b'0') as u32;█cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));█}█b';' => {█params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);█cur = None;█}█b'h' | b'l' => {█if let Some(v) = cur {█params.push(v.min(u16::MAX as u32) as u16);█}█return DecsetParse::Complete {█consumed: j + 1,█params,█set: s[j] == b'h',█};█}█// Any other byte => this isn't a clean private-mode set/reset.█_ => return DecsetParse::NotDecset,█}█j += 1;█}█...█}█[Showing lines 1-183 of 3214. Use :184 to continue]\u000f█</out>█# Tool call ¶█//Finding rc stdin pump symbols█grep(pattern=\"spawn_stdin|stdin.*reader|send_attach_input|stdin_rx|read\\\\(&mut|write_all|detach\", path=\"crates/spt/src/rc.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 12|//!█*13|//! **Detach never kills the session.** The broker owns the PTY; closing our send█14|//! side ends the viewport with the session still running (`serve_attach` ->█*15|//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS█16|//! attach-lifecycle invariant).█17|//!█18|//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread█*19|//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel█*20|//! (and watches for the detach keybind); the main loop interleaves draining that█*21|//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.█22|// [impl->REQ-RC-1]█23|█24|use std::io::{Read, Write};█...█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;█...█34|█*35|/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the█*36|/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one█37|/// literal ctrl-b to the session; any other byte after the prefix is [...24882ch elided...] er.feed(plain) {█*3185| sink.write_all(&status.assert_bytes()).unwrap();█3186| }█3187| assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");█3188|█...█3196| fired = true;█*3197| sink.write_all(&status.assert_bytes()).unwrap();█3198| }█3199| assert!(fired, \"the alt-screen enter trips the re-assert scanner\");█3200| assert!(sink.len() > before, \"a re-assert was appended after the trigger\");█...█3206| // (c) pump EXIT: restore (reset region + clear row 1).█*3207| sink.write_all(&StatusRow::restore_bytes()).unwrap();█3208| assert!(█3209| sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),█3210| \"exit restores the full scroll region and clears the reserved row\"\u000f█</out>█# Tool call ¶█//Reading physical conn regression gate█read(path=\"crates/spt-daemon/tests/brain_decouple.rs\", selector=\"430-835\")█<out>█\u000efn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {█SpawnReq {█...█args: vec![█...█.to_string(),█],█rows: 24,█cols: 80,█endpoint: endpoint.to_string(),█cwd: None,█env: Default::default(),█translation_binary: None,█adapter: String::new(),█install_dir: None,█}█}█/// One `KIND_SESSIONS` round-trip, returning the full reply. The dispatch runs█/// `reap_dead_controller` -> the time-based `stall_evict_controller`, so polling█/// this both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.█/// Bounded thread + channel (a wedge surfaces as a timeout, not a hang), like the█/// existing [`stall_evict_count`] helper. The transient conn is dropped when the█/// thread returns.█#[cfg(windows)]█fn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {█let name = name.to_string();█let (tx, rx) = std::sync::mpsc::channel();█thread::spawn(move || {█let mut c = connect(&name);█send(&mut c, spt_daemon::msg::KIND_SESSIONS, serde_json::Value::Null);█loop {█match read_frame(&mut c) {█Ok( [...16433ch elided...] old_client_write_is_err,█Some(true),█\"a write on the OLD client connection must FAIL once the physical conn is \\█retired. PRE-FIX RED: the conn is still alive so the write succeeds (Ok).\"█);█// The fresh controller resumes from the frozen cursor with NO lost frame.█assert!(█matches!(█s9_outcome,█Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)█),█\"a fresh controller from the frozen cursor must take control; got {s9_outcome:?}\"█);█assert_eq!(█s9_first_seq,█Some(resume_seq),█\"the fresh controller's FIRST replayed frame seq must equal the frozen resume \\█cursor (no lost frame); got first_seq={s9_first_seq:?} resume_seq={resume_seq}\"█);█}█\u000f█</out>█# Tool call ¶█//Reading blackhole lifecycle gate█read(path=\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\", selector=\"1-330,560-650\")█<out>█\u000e//! MSG-IDENTITY W6 / F-039 leg (e) - the DETERMINISTIC BLACK-HOLED-CONTROLLER█//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker█//! hosting REAL PTY children over the real local IPC.█//!█//! ## What this is█//!█//! This is hertz's RCA fix-shape item 5 made executable (`.claude/█//! hertz-pty-freeze-rca.md` §Proposed fix shape, item 5): the standing█//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the█//! EXACT Windows named-pipe black-hole (a controller that stops draining while a█//! finite burst floods its socket, so the broker's writer parks inside█//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` - no supported█//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/█//! src/conn.rs` bounded/cancelable/poison-on-failure writes, landed fc048bb),█//! asserts hertz's FIVE invariants for ONE incident:█//!█//! 1. UNRELATED SESSIONS CONTINUE - a second, independent session's viewer keeps█//! receiving output throughout the black-hole incident (the fan-out never█//! blocks the whole broker on the one wedged conn);█//! 2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE B [...17132ch elided...] INVARIANT 1: the UNRELATED session's viewer must keep receiving output \\█ACROSS the black-hole incident - the fan-out must never block the whole \\█broker on the one wedged conn; got u_baseline={u_baseline} u_final={u_final}\"█);█// -- INVARIANT 2: THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE█// BOUND - a write on the OLD client conn FAILS (it was physically retired,█// not merely logically stall-evicted). --█assert_eq!(█old_client_write_is_err,█Some(true),█\"INVARIANT 2: a write on the OLD black-holed client connection must FAIL - \\█the physical conn must be canceled/closed within the write bound of logical \\█release. A success means the conn is still physically alive (the r4 RED).\"█);█...█}\u000f█</out>"}}}
{"type":"message","id":"e106c434","parentId":"ccbb314e","timestamp":"2026-07-16T09:58:08.086Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting brainproc cursor calls and update tests**"},{"type":"toolCall","id":"call_D7cMENeekU3MWc8sXp8uvIZY|fc_07547ad47f060e5d016a58ab2f8d08819aaa2b8d687a2fe78c","name":"read","arguments":{"i":"Reading binary swap restart test","path":"crates/spt/tests/brain_respawn_rename.rs","selector":"1-185"}},{"type":"toolCall","id":"call_Fn2dKgZ7Ee3RnnFxUvChJhxf|fc_07547ad47f060e5d016a58ab2f8e4c819abf49f494a63ac9b9","name":"read","arguments":{"i":"Reading controlled session refresh rig","path":"crates/spt/tests/resume_no_control_steal_e2e.rs","selector":"1-100,185-340"}},{"type":"toolCall","id":"call_PrYIZjsVfkaTd2PtFcNCsn7f|fc_07547ad47f060e5d016a58ab2f8e54819abe1cb96104cb6646","name":"read","arguments":{"i":"Reading drain drive update rig","path":"crates/spt/tests/trial_drain_drive_e2e.rs","selector":"1-120,430-550"}},{"type":"toolCall","id":"call_Pcj7i5r6TpUSV18VSfKMlGHd|fc_07547ad47f060e5d016a58ab2f8ebc819ab77a68eb36db63b1","name":"read","arguments":{"i":"Reading brain connection deadlock tests","path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","selector":"1-120,540-740"}},{"type":"toolCall","id":"call_BLnGGLEZzrZ9dkKpWATI7ql0|fc_07547ad47f060e5d016a58ab2f8ecc819ab70d88160c233bce","name":"read","arguments":{"i":"Reading attach integration helpers","path":"crates/spt-daemon/tests/attach.rs","selector":"1-120,720-840,1000-1140"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":47673,"output":303,"cacheRead":0,"cacheWrite":0,"totalTokens":47976,"reasoningTokens":63,"cost":{"input":0.23836500000000002,"output":0.00909,"cacheRead":0,"cacheWrite":0,"total":0.24745500000000004}},"stopReason":"toolUse","timestamp":1784195878154,"responseId":"resp_07547ad47f060e5d016a58ab26bd40819aa187b17c1f1fb98c","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab2a0d58819a80453b753ef51f33","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsvbGVNe0MMP8ke7-sG7ZEYB95J0_f1IaVXeuU5jHClu8kwr4A9OGCBXiUi7e_Fv_gVFxj837wP-1RRljIQbOnzDKCwAvF05oQ_zN2RNKmcwRmUuuZI3kiPoxCeRrEK5gf2cdmBPG_aN22rUKMXcBdtaPf3fThOE5xyBgdVbUT0OLZX6627h5F-faaoPYhqyrjzWnCDs9oXksFTvTZI3JlwCzElf2VZbiJ6u_qm3BSWT-0iODHa_lD7QLS6wD21v_7ddgrmzC4OXPgDOonb7fEMlo0hXVaUrDnAM5V0rTUNRPgyBE1PrGWska0N8APkiYINN6WD6LrLDECFdiOofg74od7gHbvkOgy_8C6vY9rxhb3oeHkQHHUU-4g6pIM7TtPedh9AMZ7zzeRzCBXuvWylwqozyO4uMpw8NxAQRBpFyZegmkbF0t0Bik7vLw6A-gj4Wh5Vghd6angXSS7ktC3KZxAtcgOPSed_0xTkjDLzDcM58F4R0WGfDQzn3QmLYUSf2ZucCOGAAGVgm5Y6tkYNZNR9-VZF7pUmV3ToHHr1XTnyOZZwUhzR9UqfuS93mWbNzpGi_2Ie4g8FKYfXqp-YqLA-XQwmGWa-W84TyuiZydTR2C5hCASGXTY7Kbrx_rwIS2yylkzQJ_sU7KVI66mR_2lUpPjlp_nJHgw8CEsMkRP8yaBQIMfb0SfBQMqibaj5UudEMKlj3FAbbxabWfSUvhPbAtErTJMDpErAVPrJSlL43w1TVgLAXDp9Aajll4N7Cc7wh_yneOJk85tWPJVspLzo5wVsy5c3-fmEG_52JFWUFhSv63yAWa6X60gpr3nFAFaXIkhEb4bgKXRVAi2ZkTqtU7OepJU2D816eL6NsHjex2lxwQOg_2xpee89k-4L2FIsNM7Ed6KY1JN4FqNdrzPFa33fXXPnzkJWGH30gykag4iawihuQsMy9WeE_WDYR4SFxGdpwztTHvGuF9RFVKtyTWLkfCnMCkq0Vqy24b8Ds2_PhGbNzONP0On2yGtTuuVZmLdUPW58w7A0Jp6eSsQwAbV7jE9MyvAiUyCACGRiTPDG1i9nWH8dtrV-uo4v5x872mzQetaS8m6-6bDs8BLSJaxlPBZAQjOgxu_PiUQmXB1dQrtjZZnwFDuniMI0JIA0olBo0Eb6u71J9eu_E4GGGULv9qv7V5GL_tMp0cNz_eH1odi00-I_NTCqhfKJEy1tGZAVi5aVa9LZuXDCkiudTDTLI0O7jrIawZHVIhq1Jz4-TZ_VgeNOwew77OT6-1thdwP4eQpSZ9NGPgp1vk3lvO3DFXyi5gw1rp4PMhc=","summary":[{"type":"summary_text","text":"**Inspecting brainproc cursor calls and update tests**"}]},{"id":"fc_07547ad47f060e5d016a58ab2f8d08819aaa2b8d687a2fe78c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading binary swap restart test\",\"path\":\"crates/spt/tests/brain_respawn_rename.rs\",\"selector\":\"1-185\"}","call_id":"call_D7cMENeekU3MWc8sXp8uvIZY","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab2f8e4c819abf49f494a63ac9b9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controlled session refresh rig\",\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"selector\":\"1-100,185-340\"}","call_id":"call_Fn2dKgZ7Ee3RnnFxUvChJhxf","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab2f8e54819abe1cb96104cb6646","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading drain drive update rig\",\"path\":\"crates/spt/tests/trial_drain_drive_e2e.rs\",\"selector\":\"1-120,430-550\"}","call_id":"call_PrYIZjsVfkaTd2PtFcNCsn7f","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab2f8ebc819ab77a68eb36db63b1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain connection deadlock tests\",\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"selector\":\"1-120,540-740\"}","call_id":"call_Pcj7i5r6TpUSV18VSfKMlGHd","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab2f8ecc819ab70d88160c233bce","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach integration helpers\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"1-120,720-840,1000-1140\"}","call_id":"call_BLnGGLEZzrZ9dkKpWATI7ql0","name":"read"}]},"duration":9931.178199999966,"ttft":3877.349700000137,"contextSnapshot":{"promptTokens":47673,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_D7cMENeekU3MWc8sXp8uvIZY|fc_07547ad47f060e5d016a58ab2f8d08819aaa2b8d687a2fe78c","toolName":"read","startedAt":"2026-07-16T09:58:08.087Z","args":{"path":"crates/spt/tests/brain_respawn_rename.rs"},"intent":"Reading binary swap restart test"},"id":"ab2c10c8","parentId":"e106c434","timestamp":"2026-07-16T09:58:08.087Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Fn2dKgZ7Ee3RnnFxUvChJhxf|fc_07547ad47f060e5d016a58ab2f8e4c819abf49f494a63ac9b9","toolName":"read","startedAt":"2026-07-16T09:58:08.087Z","args":{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs"},"intent":"Reading controlled session refresh rig"},"id":"256a00f1","parentId":"ab2c10c8","timestamp":"2026-07-16T09:58:08.087Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PrYIZjsVfkaTd2PtFcNCsn7f|fc_07547ad47f060e5d016a58ab2f8e54819abe1cb96104cb6646","toolName":"read","startedAt":"2026-07-16T09:58:08.087Z","args":{"path":"crates/spt/tests/trial_drain_drive_e2e.rs"},"intent":"Reading drain drive update rig"},"id":"5965cfe9","parentId":"256a00f1","timestamp":"2026-07-16T09:58:08.087Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Pcj7i5r6TpUSV18VSfKMlGHd|fc_07547ad47f060e5d016a58ab2f8ebc819ab77a68eb36db63b1","toolName":"read","startedAt":"2026-07-16T09:58:08.087Z","args":{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs"},"intent":"Reading brain connection deadlock tests"},"id":"47f028f1","parentId":"5965cfe9","timestamp":"2026-07-16T09:58:08.087Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BLnGGLEZzrZ9dkKpWATI7ql0|fc_07547ad47f060e5d016a58ab2f8ecc819ab70d88160c233bce","toolName":"read","startedAt":"2026-07-16T09:58:08.087Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading attach integration helpers"},"id":"6d1c68e3","parentId":"47f028f1","timestamp":"2026-07-16T09:58:08.087Z"}
{"type":"message","id":"a00fee7b","parentId":"6d1c68e3","timestamp":"2026-07-16T09:58:08.092Z","message":{"role":"toolResult","toolCallId":"call_Fn2dKgZ7Ee3RnnFxUvChJhxf|fc_07547ad47f060e5d016a58ab2f8e4c819abf49f494a63ac9b9","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//! brain derives `broker_socket_name()` from it — the brain_survive.rs / trial_drain_drive\n//! discipline). A SECOND `#[test]` MUST bring env-lock serialization or it races this one.\n//! `cargo nextest run --test resume_no_control_steal_e2e` or\n//! `cargo test --test resume_no_control_steal_e2e -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\n//!\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows installer-detection\n//! demands UAC elevation for such an exe → `os error 740` at `--list` (KH win-update-exe-\n//! uac740). This file is deliberately `resume_no_control_steal_e2e`.\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::endpoint::broker_socket_name;\n…\n/// no discovery, loopback-only — no real interface, no firewall prompt. Net enabled so the\n/// subprocess brain boots the proven production way.\nfn hermetic(identity: Identity) -> NetConfig {\n    NetConfig {\n        identity,\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\n/// Bind a net-enabled broker (serving on a background thread) at `name`.\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n        .expect(\"bind broker with net\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\n/// Connect a raw brain-role conn to the broker (retrying while it finishes binding). The\n/// concrete stream is `Send + 'static` so the per-session reader thread can own it.\nfn connect(name: &str) -> impl std::io::Read + std::io::Write + Send + 'static {\n    for _ in 0..300 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Spawn one ticker session over a fresh controller conn and start a reader thread that\n/// tallies its `KIND_OUTPUT` frames. Returns `(session_id, tick_counter)`. `dispatch_spawn`\n/// pre-attaches this conn as the session's LOCAL (by:None) controller, so the tally IS the\n/// controller's live output feed — it FREEZES the instant a resume-steal displaces it.\nfn spawn_ticker_controller(name: &str, endpoint: &str) -> (u64, Arc<AtomicU64>) {\n    let mut c = connect(name);\n    send(\n        &mut c,\n        KIND_SPAWN,\n        serde_json::to_value(ticker_spawn_req(endpoint)).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut c).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    let ticks = Arc::new(AtomicU64::new(0));\n    let ticks_reader = Arc::clone(&ticks);\n    // The reader owns the conn for the rest of the test; process-per-test exit reaps it.\n    // A dropped controller sink (RED steal) stops the broker delivering to this conn — the\n    // read either EOFs (thread exits) or simply starves; either way the tally freezes.\n    thread::spawn(move || {\n        let mut c = c;\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    ticks_reader.fetch_add(1, Ordering::Relaxed);\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n    });\n    (sid, ticks)\n}\n\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + RAN resume_sessions +\n/// stamped ready — resume completes BEFORE ready, so a present ready means the steal, if\n/// any, has already happened). Returns that pid, or `None` if it never appears in `budget`.\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some(pid) = std::fs::read_to_string(path)\n            .ok()\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\n        {\n            return Some(pid as u32);\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads REAL\n/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\nstruct ResumeTrialEnv {\n    broker: Arc<Broker>,\n    ready_path: PathBuf,\n    promotions: Mutex<Vec<u64>>,\n    rollbacks: Mutex<Vec<u64>>,\n    stop: Arc<AtomicBool>,\n}\n\nimpl TrialEnv for ResumeTrialEnv {\n    fn applied_state(&self) -> Option<AppliedRecord> {\n        Some(AppliedRecord::AppliedPending {\n            version: 9,\n            rollback_binary: \"/good/spt.old-9\".to_string(),\n            candidate_started_ms: 0,\n            prior_version: None,\n        })\n    }\n    fn ready_generation(&self) -> Option<u64> {\n        ready_generation_at(&self.ready_path)\n    }\n    fn clear_ready(&self) {\n        let _ = std::fs::remove_file(&self.ready_path);\n    }\n    fn record_promoted(&self, version: u64) {\n        self.promotions.lock().unwrap().push(version);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\n        self.rollbacks.lock().unwrap().push(quarantine);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn ready_exe_hash(&self) -> Option<String> {\n        None\n    }\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n        None\n    }\n    // No wedge in this rig → drained is true → the latched-ready candidate promotes.\n    fn old_gen_drained(&self) -> bool {\n        !self.broker.any_local_controller_wedged()\n    }\n}\n\n/// Raise `stop` (so the supervisor kills any live brain child and exits) and panic with\n/// `msg` — keeps a failing assertion from orphaning the subprocess brain + the children.\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n    stop.store(true, Ordering::SeqCst);\n    panic!(\"{msg}\");\n}\n\n// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n#[test]\nfn brain_respawn_keeps_every_session_controller_and_still_promotes() {\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    std::env::set_var(\"SPT_HOME\", home.path());\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//! brain derives `broker_socket_name()` from it — the brain_survive.rs / trial_drain_drive\n//! discipline). A SECOND `#[test]` MUST bring env-lock serialization or it races this one.\n//! `cargo nextest run --test resume_no_control_steal_e2e` or\n//! `cargo test --test resume_no_control_steal_e2e -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\n//!\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows installer-detection\n//! demands UAC elevation for such an exe → `os error 740` at `--list` (KH win-update-exe-\n//! uac740). This file is deliberately `resume_no_control_steal_e2e`.\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::endpoint::broker_socket_name;\n…\n/// no discovery, loopback-only — no real interface, no firewall prompt. Net enabled so the\n/// subprocess brain boots the proven production way.\nfn hermetic(identity: Identity) -> NetConfig {\n    NetConfig {\n        identity,\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\n/// Bind a net-enabled broker (serving on a background thread) at `name`.\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\n    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n        .expect(\"bind broker with net\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn send<W: std::io::Write>(conn: &mut W, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\n/// Connect a raw brain-role conn to the broker (retrying while it finishes binding). The\n/// concrete stream is `Send + 'static` so the per-session reader thread can own it.\nfn connect(name: &str) -> impl std::io::Read + std::io::Write + Send + 'static {\n    for _ in 0..300 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Spawn one ticker session over a fresh controller conn and start a reader thread that\n/// tallies its `KIND_OUTPUT` frames. Returns `(session_id, tick_counter)`. `dispatch_spawn`\n/// pre-attaches this conn as the session's LOCAL (by:None) controller, so the tally IS the\n/// controller's live output feed — it FREEZES the instant a resume-steal displaces it.\nfn spawn_ticker_controller(name: &str, endpoint: &str) -> (u64, Arc<AtomicU64>) {\n    let mut c = connect(name);\n    send(\n        &mut c,\n        KIND_SPAWN,\n        serde_json::to_value(ticker_spawn_req(endpoint)).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut c).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    let ticks = Arc::new(AtomicU64::new(0));\n    let ticks_reader = Arc::clone(&ticks);\n    // The reader owns the conn for the rest of the test; process-per-test exit reaps it.\n    // A dropped controller sink (RED steal) stops the broker delivering to this conn — the\n    // read either EOFs (thread exits) or simply starves; either way the tally freezes.\n    thread::spawn(move || {\n        let mut c = c;\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    ticks_reader.fetch_add(1, Ordering::Relaxed);\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n    });\n    (sid, ticks)\n}\n\n/// Poll `brain.ready` until it holds a pid (the subprocess booted + RAN resume_sessions +\n/// stamped ready — resume completes BEFORE ready, so a present ready means the steal, if\n/// any, has already happened). Returns that pid, or `None` if it never appears in `budget`.\nfn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some(pid) = std::fs::read_to_string(path)\n            .ok()\n            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\n            .and_then(|v| v.get(\"pid\").and_then(|p| p.as_u64()))\n        {\n            return Some(pid as u32);\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads REAL\n/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\nstruct ResumeTrialEnv {\n    broker: Arc<Broker>,\n    ready_path: PathBuf,\n    promotions: Mutex<Vec<u64>>,\n    rollbacks: Mutex<Vec<u64>>,\n    stop: Arc<AtomicBool>,\n}\n\nimpl TrialEnv for ResumeTrialEnv {\n    fn applied_state(&self) -> Option<AppliedRecord> {\n        Some(AppliedRecord::AppliedPending {\n            version: 9,\n            rollback_binary: \"/good/spt.old-9\".to_string(),\n            candidate_started_ms: 0,\n            prior_version: None,\n        })\n    }\n    fn ready_generation(&self) -> Option<u64> {\n        ready_generation_at(&self.ready_path)\n    }\n    fn clear_ready(&self) {\n        let _ = std::fs::remove_file(&self.ready_path);\n    }\n    fn record_promoted(&self, version: u64) {\n        self.promotions.lock().unwrap().push(version);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn record_rolled_back(&self, quarantine: u64, _running: u64, _bin: &str) {\n        self.rollbacks.lock().unwrap().push(quarantine);\n        self.stop.store(true, Ordering::Relaxed);\n    }\n    fn ready_exe_hash(&self) -> Option<String> {\n        None\n    }\n    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n        None\n    }\n    // No wedge in this rig → drained is true → the latched-ready candidate promotes.\n    fn old_gen_drained(&self) -> bool {\n        !self.broker.any_local_controller_wedged()\n    }\n}\n\n/// Raise `stop` (so the supervisor kills any live brain child and exits) and panic with\n/// `msg` — keeps a failing assertion from orphaning the subprocess brain + the children.\nfn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n    stop.store(true, Ordering::SeqCst);\n    panic!(\"{msg}\");\n}\n\n// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n#[test]\nfn brain_respawn_keeps_every_session_controller_and_still_promotes() {\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    std::env::set_var(\"SPT_HOME\", home.path());\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,null,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,null,516]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\resume_no_control_steal_e2e.rs"}}},"isError":false,"timestamp":1784195888092}}
{"type":"message","id":"3ff8dd1d","parentId":"a00fee7b","timestamp":"2026-07-16T09:58:08.095Z","message":{"role":"toolResult","toolCallId":"call_PrYIZjsVfkaTd2PtFcNCsn7f|fc_07547ad47f060e5d016a58ab2f8e54819abe1cb96104cb6646","toolName":"read","content":[{"type":"text","text":"//! UPDATE-WEDGE counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) — the A' int rig\n//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\n//! production `run_brain` self-drives the broker's controller-liveness reap so a\n//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\n//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\n//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\n//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\n//! operator take; here the candidate is the real subprocess whose OWN heartbeat\n//! `Brain::sessions()` drives the reap — the exact production line the 2026-07-09\n//! field freeze proved missing.\n//!\n//! ## The field freeze this locks down (root, log-anchored)\n//!\n//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\n//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\n//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\n//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\n//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\n//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\n//! OWN trial with no coordinated broker restart.\n//!\n//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\n//!\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\n//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\n//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\n//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\n//! only makes this rig MORE faithful; it still establishes the wedge after boot for\n//! continuity. The historical `resume_sessions()` framing is retained below for context.\n//!\n//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\n//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\n//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\n//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\n//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\n//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\n//! flood session and black-holes as its by:None controller — a session the boot-once\n//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\n//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\n//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\n//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\n//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\n//! read BROKER-side, in THIS process's in-proc broker where the reap + the drained read\n//! both run.\n//!\n//! ## The DRAINED gate is held open deterministically (the ready latch)\n//!\n//! The real subprocess stamps `brain.ready` autonomously the instant it boots — before\n//! the test can establish the wedge. So the rig's [`TrialEnv::ready_generation`]\n//! WITHHOLDS the candidate's readiness from `run_trial` until the test ARMS it (after\n//! the old-gen wedge is up + matured), then reads the REAL `brain.ready` file. This is\n//! the process-level analog of false_promote's scripted-ready `Mutex`: it guarantees\n//! the DRAINED gate has a matured wedge to block on when readiness is first seen, so\n//! the promote is a genuine drive-cleared-the-wedge, never a trivial no-wedge promote.\n//!\n//! ## What this rig asserts — BOTH legs are \"the brain's OWN self-drive clears it\"\n//!\n//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\n//!   wedge and the candidate ready, the trial PROMOTES within the window — the\n//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\n//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\n//!   before commit): comment out the production drive `let _ = brain.sessions();`\n//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\n//!   test rolls back instead (`WindowElapsedAlive` → the field freeze) — proving the\n//!   drive is load-bearing. For the RED to bite, the run_brain heartbeat must be the\n//!   ONLY `KIND_SESSIONS` driver: the subprocess brain's livehost reconcile loop also\n//!   polls `brain.sessions()` every 5s and would drive the same reap, so the rig sets\n//!   `SPT_LIVEHOST_RECONCILE_DISABLE` to silence it — faithfully reproducing the field's\n//!   livehost-silent update-trial (PIN Q2: the trial-brain livehost's first reconcile\n//!   tick landed around the 30s kill), the exact condition the fix makes the reap\n//!   independent of.**\n//! - **Leg 2 (lighter, doyle FINALIZE refinement).** Assert the broker's stall-evict\n//!   tally INCREMENTED — a reap actually FIRED via the brain's own drive (the test never\n//!   polls `KIND_SESSIONS` itself, so the subprocess candidate is the ONLY driver). We\n//!   do NOT assert zero evicts: the stall-evict IS the reap mechanism, so a\n//!   count==0 assertion would be asserting the fix does not work. The recovery\n//!   (crash-reason) brain runs the IDENTICAL `run_brain` heartbeat, so this tally —\n//!   confirmed by the promote — is the same-code-path evidence that a rolled-back brain\n//!   also self-heals its control plane promptly on its own heartbeat, not an external poll.\n//!\n//! ## RUNNER CONTRACT — single `#[test]`, run process-per-test (nextest) or -j1\n//!\n//! This is the SOLE `#[test]` here by design: it owns the process-global `SPT_HOME`\n//! (the subprocess brain derives `broker_socket_name()` from it, so the in-proc broker\n//! and the child must share ONE home — the brain_survive.rs discipline). A SECOND\n//! `#[test]` added here MUST bring env-lock serialization or it races this one. Run with\n//! `cargo nextest run --test trial_drain_drive_e2e` (CI's path) or\n//! `cargo test --test trial_drain_drive_e2e -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\n//!\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\n//! installer-detection heuristics demand UAC elevation for a test exe whose name\n//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\n//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brain::Brain;\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::endpoint::broker_socket_name;\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::relcache::AppliedRecord;\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\n\n…\nfn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\n…\n    send(\n…\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    // A parked long past the test; process-per-test exit reaps it.\n    thread::spawn(move || {\n        thread::sleep(Duration::from_secs(120));\n        drop(a);\n    });\n\n    // ── Confirm the wedge FORMED before arming — DETERMINISTIC (a fixed sleep races\n    //    formation: the flood child can be slow to start flooding, so conn A may not be\n    //    write-blocked yet). The wedge is proven real once EITHER the broker reads it\n    //    wedged (matured, not-yet-reaped) OR a stall-evict has fired since we spawned A\n    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\n    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\n    //    masking the very regression the RED must catch. ──\n    let baseline_evicts = read_evicts(&broker_socket_name());\n    let form_deadline = Instant::now() + Duration::from_secs(30);\n    let formed = loop {\n        if broker.any_local_controller_wedged()\n            || read_evicts(&broker_socket_name()) > baseline_evicts\n        {\n            break true;\n        }\n        if Instant::now() >= form_deadline {\n            break false;\n        }\n        thread::sleep(Duration::from_millis(50));\n    };\n    if !formed {\n        teardown_panic(\n            &stop,\n            \"PRECONDITION: the old-gen controller wedge never formed (the flood child \\\n             never write-blocked conn A) — the rig cannot exercise the DRAINED gate\",\n        );\n    }\n\n    // ── ARM: run_trial may now observe the candidate's readiness. With a matured\n    //    old-gen wedge, the DRAINED gate blocks promotion UNTIL the candidate's OWN\n    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\n    //    commented out): nothing reaps it → old_gen_drained stays false → the window\n    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\n    env.armed.store(true, Ordering::Relaxed);\n\n    // Wait out the trial verdict: GREEN promotes within a heartbeat of arming; RED\n    // (no drive, livehost silenced) never drains, so run_trial only rolls back when its\n    // window elapses — so this ceiling MUST exceed the trial window to OBSERVE the RED\n    // rollback (GREEN still exits in ~1 heartbeat, so the long ceiling costs nothing).\n    let deadline = Instant::now() + Duration::from_secs(75);\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    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\n    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\n    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\n    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\n    //    zero — the evict IS the reap. ──\n    let evicts = read_evicts(&broker_socket_name());\n\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess\n    //    brain + the flood child). stop kills the live brain child; join reaps it. ──\n    stop.store(true, Ordering::Relaxed);\n    if let Some(pid) = broker.session_pid(sid) {\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    eprintln!(\n        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\n         rollbacks={rollbacks:?} stall_evicts={evicts} \\\n         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\n    );\n\n    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\n    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n    assert!(\n        promoted,\n        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n         the window. RED (comment the production `let _ = brain.sessions();` in \\\n         brainproc.rs, rebuild): nothing drives the reap → the wedge strands the DRAINED \\\n         gate → WindowElapsedAlive → rollback (the 2026-07-09 field freeze).\\n\\\n         rollbacks={rollbacks:?}\"\n    );\n    assert_eq!(\n        promotions.as_slice(),\n        &[9],\n        \"promoted the candidate version exactly once, after the self-drive reap\"\n    );\n    assert!(\n        rollbacks.is_empty(),\n        \"no rollback — the self-drive reap drained the old gen inside the window\"\n    );\n    // Leg 2 (lighter, doyle FINALIZE): a stall-evict FIRED via the brain's own drive —\n    // proof the wedge existed and the self-drive (the identical loop the recovery brain\n    // runs) reaped it, not a trivial no-wedge promote. NOT count==0 (the evict IS the reap).\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n    assert!(\n        evicts >= 1,\n        \"the broker's stall-evict tally must show a reap FIRED via the candidate's own \\\n         sessions() drive (the test never polls KIND_SESSIONS itself) — proof the wedge \\\n         was real and the self-drive cleared it; got {evicts}\"\n    );\n}\n"}],"details":{"displayContent":{"text":"//! UPDATE-WEDGE counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) — the A' int rig\n//! (doyle FINALIZE 2026-07-09): a REAL `spt daemon brain` CHILD PROCESS running\n//! production `run_brain` self-drives the broker's controller-liveness reap so a\n//! hard-killed prior generation's black-holed LOCAL controller conn cannot strand\n//! the promotion DRAINED gate. This is the process-level upgrade of the in-process\n//! `crates/spt-daemon/tests/false_promote.rs` (REQ-UPDATE-PROMOTE-DRAINED): there the\n//! candidate is a fake `sleep`/`ping` and the wedge is drained by an EXTERNAL\n//! operator take; here the candidate is the real subprocess whose OWN heartbeat\n//! `Brain::sessions()` drives the reap — the exact production line the 2026-07-09\n//! field freeze proved missing.\n//!\n//! ## The field freeze this locks down (root, log-anchored)\n//!\n//! `spt update fetch --apply` (v0.30.0 → v0.30.2) hard-killed the outgoing brain;\n//! its live-agent controller conn black-holed (a killed peer's pipe BLOCKS the\n//! broker-side writer rather than EOFing) → `any_local_controller_wedged()` read\n//! true → the new candidate's promotion DRAINED gate (`run_trial` →\n//! `env.old_gen_drained()`) stayed false for the full 30s trial → \"alive but never\n//! ready\" → auto-rollback. `old_gen_drained()` is a PURE READ of `write_blocked_since`\n//! — nothing in the isolated trial window DROVE the evict (`stall_evict_controller`\n//! only runs inside the broker's `KIND_SESSIONS` reap sweep, which nothing polled).\n//! The fix: `run_brain`'s heartbeat loop issues `Brain::sessions()` every tick, so the\n//! candidate DRIVES the live older broker's already-shipped reap and un-strands its\n//! OWN trial with no coordinated broker restart.\n//!\n//! ## Why the wedge is established AFTER the candidate boots (the resume paradox)\n//!\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\n//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\n//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\n//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\n//! only makes this rig MORE faithful; it still establishes the wedge after boot for\n//! continuity. The historical `resume_sessions()` framing is retained below for context.\n//!\n//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\n//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\n//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\n//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n//! hosts no PTY sessions; brainproc.rs:184) and the wedged controller was a\n//! prior-generation conn resume did NOT cover. This rig reproduces that faithfully:\n//! the candidate boots + resumes against an EMPTY session set, THEN conn A spawns a\n//! flood session and black-holes as its by:None controller — a session the boot-once\n//! resume never covered, exactly the field's un-resumed wedge. A black-holed by:None\n//! conn is a faithful analog of the field's live-host controller: the DRAINED gate\n//! reads only `by.is_none()` + `write_blocked_since`, so it cannot tell the two apart\n//! (doyle FINALIZE). `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15s maturity to ~800ms\n//! (the knob grants wall-clock only — it never relaxes an assertion); the deadline is\n//! read BROKER-side, in THIS process's in-proc broker where the reap + the drained read\n//! both run.\n//!\n//! ## The DRAINED gate is held open deterministically (the ready latch)\n//!\n//! The real subprocess stamps `brain.ready` autonomously the instant it boots — before\n//! the test can establish the wedge. So the rig's [`TrialEnv::ready_generation`]\n//! WITHHOLDS the candidate's readiness from `run_trial` until the test ARMS it (after\n//! the old-gen wedge is up + matured), then reads the REAL `brain.ready` file. This is\n//! the process-level analog of false_promote's scripted-ready `Mutex`: it guarantees\n//! the DRAINED gate has a matured wedge to block on when readiness is first seen, so\n//! the promote is a genuine drive-cleared-the-wedge, never a trivial no-wedge promote.\n//!\n//! ## What this rig asserts — BOTH legs are \"the brain's OWN self-drive clears it\"\n//!\n//! - **Leg 1 (load-bearing, RED-first).** With a matured old-gen by:None controller\n//!   wedge and the candidate ready, the trial PROMOTES within the window — the\n//!   subprocess's own heartbeat `Brain::sessions()` drove the reap, `old_gen_drained()`\n//!   flipped, and the latched ready promoted. **RED-first (dev discipline, verified\n//!   before commit): comment out the production drive `let _ = brain.sessions();`\n//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\n//!   test rolls back instead (`WindowElapsedAlive` → the field freeze) — proving the\n//!   drive is load-bearing. For the RED to bite, the run_brain heartbeat must be the\n//!   ONLY `KIND_SESSIONS` driver: the subprocess brain's livehost reconcile loop also\n//!   polls `brain.sessions()` every 5s and would drive the same reap, so the rig sets\n//!   `SPT_LIVEHOST_RECONCILE_DISABLE` to silence it — faithfully reproducing the field's\n//!   livehost-silent update-trial (PIN Q2: the trial-brain livehost's first reconcile\n//!   tick landed around the 30s kill), the exact condition the fix makes the reap\n//!   independent of.**\n//! - **Leg 2 (lighter, doyle FINALIZE refinement).** Assert the broker's stall-evict\n//!   tally INCREMENTED — a reap actually FIRED via the brain's own drive (the test never\n//!   polls `KIND_SESSIONS` itself, so the subprocess candidate is the ONLY driver). We\n//!   do NOT assert zero evicts: the stall-evict IS the reap mechanism, so a\n//!   count==0 assertion would be asserting the fix does not work. The recovery\n//!   (crash-reason) brain runs the IDENTICAL `run_brain` heartbeat, so this tally —\n//!   confirmed by the promote — is the same-code-path evidence that a rolled-back brain\n//!   also self-heals its control plane promptly on its own heartbeat, not an external poll.\n//!\n//! ## RUNNER CONTRACT — single `#[test]`, run process-per-test (nextest) or -j1\n//!\n//! This is the SOLE `#[test]` here by design: it owns the process-global `SPT_HOME`\n//! (the subprocess brain derives `broker_socket_name()` from it, so the in-proc broker\n//! and the child must share ONE home — the brain_survive.rs discipline). A SECOND\n//! `#[test]` added here MUST bring env-lock serialization or it races this one. Run with\n//! `cargo nextest run --test trial_drain_drive_e2e` (CI's path) or\n//! `cargo test --test trial_drain_drive_e2e -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids are reaped SCOPED (never machine-wide).\n//!\n//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\n//! installer-detection heuristics demand UAC elevation for a test exe whose name\n//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\n//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\n\nuse std::path::PathBuf;\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::brain::Brain;\nuse spt_daemon::brainproc::{ready_generation_at, supervise_brain, BrainRestart, StartReason, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::endpoint::broker_socket_name;\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::relcache::AppliedRecord;\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\n\n…\nfn trial_candidate_self_drives_the_reap_of_a_black_holed_old_gen_controller() {\n…\n    send(\n…\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    // A parked long past the test; process-per-test exit reaps it.\n    thread::spawn(move || {\n        thread::sleep(Duration::from_secs(120));\n        drop(a);\n    });\n\n    // ── Confirm the wedge FORMED before arming — DETERMINISTIC (a fixed sleep races\n    //    formation: the flood child can be slow to start flooding, so conn A may not be\n    //    write-blocked yet). The wedge is proven real once EITHER the broker reads it\n    //    wedged (matured, not-yet-reaped) OR a stall-evict has fired since we spawned A\n    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\n    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\n    //    masking the very regression the RED must catch. ──\n    let baseline_evicts = read_evicts(&broker_socket_name());\n    let form_deadline = Instant::now() + Duration::from_secs(30);\n    let formed = loop {\n        if broker.any_local_controller_wedged()\n            || read_evicts(&broker_socket_name()) > baseline_evicts\n        {\n            break true;\n        }\n        if Instant::now() >= form_deadline {\n            break false;\n        }\n        thread::sleep(Duration::from_millis(50));\n    };\n    if !formed {\n        teardown_panic(\n            &stop,\n            \"PRECONDITION: the old-gen controller wedge never formed (the flood child \\\n             never write-blocked conn A) — the rig cannot exercise the DRAINED gate\",\n        );\n    }\n\n    // ── ARM: run_trial may now observe the candidate's readiness. With a matured\n    //    old-gen wedge, the DRAINED gate blocks promotion UNTIL the candidate's OWN\n    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\n    //    commented out): nothing reaps it → old_gen_drained stays false → the window\n    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\n    env.armed.store(true, Ordering::Relaxed);\n\n    // Wait out the trial verdict: GREEN promotes within a heartbeat of arming; RED\n    // (no drive, livehost silenced) never drains, so run_trial only rolls back when its\n    // window elapses — so this ceiling MUST exceed the trial window to OBSERVE the RED\n    // rollback (GREEN still exits in ~1 heartbeat, so the long ceiling costs nothing).\n    let deadline = Instant::now() + Duration::from_secs(75);\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    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\n    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\n    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\n    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\n    //    zero — the evict IS the reap. ──\n    let evicts = read_evicts(&broker_socket_name());\n\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess\n    //    brain + the flood child). stop kills the live brain child; join reaps it. ──\n    stop.store(true, Ordering::Relaxed);\n    if let Some(pid) = broker.session_pid(sid) {\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    eprintln!(\n        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\n         rollbacks={rollbacks:?} stall_evicts={evicts} \\\n         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\n    );\n\n    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\n    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n    assert!(\n        promoted,\n        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n         the window. RED (comment the production `let _ = brain.sessions();` in \\\n         brainproc.rs, rebuild): nothing drives the reap → the wedge strands the DRAINED \\\n         gate → WindowElapsedAlive → rollback (the 2026-07-09 field freeze).\\n\\\n         rollbacks={rollbacks:?}\"\n    );\n    assert_eq!(\n        promotions.as_slice(),\n        &[9],\n        \"promoted the candidate version exactly once, after the self-drive reap\"\n    );\n    assert!(\n        rollbacks.is_empty(),\n        \"no rollback — the self-drive reap drained the old gen inside the window\"\n    );\n    // Leg 2 (lighter, doyle FINALIZE): a stall-evict FIRED via the brain's own drive —\n    // proof the wedge existed and the self-drive (the identical loop the recovery brain\n    // runs) reaped it, not a trivial no-wedge promote. NOT count==0 (the evict IS the reap).\n    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n    assert!(\n        evicts >= 1,\n        \"the broker's stall-evict tally must show a reap FIRED via the candidate's own \\\n         sessions() drive (the test never polls KIND_SESSIONS itself) — proof the wedge \\\n         was real and the self-drive cleared it; got {evicts}\"\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,null,334,null,426,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\trial_drain_drive_e2e.rs"}}},"isError":false,"timestamp":1784195888095}}
{"type":"message","id":"c7fbc663","parentId":"3ff8dd1d","timestamp":"2026-07-16T09:58:08.098Z","message":{"role":"toolResult","toolCallId":"call_Pcj7i5r6TpUSV18VSfKMlGHd|fc_07547ad47f060e5d016a58ab2f8ebc819ab77a68eb36db63b1","toolName":"read","content":[{"type":"text","text":"//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n//!\n//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n//! onto the brain's OWN request/reply conn. A conn's send half is one\n//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n//! calls. When an actively-streaming session backs the conn up, the brain's own\n//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n//! wedge (both severities: respawn replay-burst + steady-state output backup), with\n//! NO dead peer required.\n//!\n//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\n//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n//! a truly deadlocked one completes ~zero).\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::brain::Brain;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\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-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\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\nfn kill_pid(pid: u32) {\n    if pid == 0 {\n        return; // unknown pid — never signal pid 0 (unix process group)\n    }\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\n/// A child that FLOODS stdout forever — the heavy-output case that fills a\n/// non-draining subscriber's conn and parks its writer inside `write_frame`.\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\n            \"/c\".to_string(),\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\n        ],\n    );\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\nfn connect(name: &str) -> Stream {\n    for _ in 0..200 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Spawn one flood PTY session; return (session_id, pid, spawner_conn). The conn is\n/// kept alive (the spawner is the local by:None controller) but NOT drained — the\n/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n/// brain's viewer subscription keeps receiving live output.\nfn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n    let mut c = connect(name);\n    write_frame(\n        &mut c,\n…\n    )\n    .expect(\"send spawn\");\n…\n}\n[Range 540-740 is beyond end of file (361 lines total); skipped]"}],"details":{"displayContent":{"text":"//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n//!\n//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n//! onto the brain's OWN request/reply conn. A conn's send half is one\n//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n//! calls. When an actively-streaming session backs the conn up, the brain's own\n//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n//! wedge (both severities: respawn replay-burst + steady-state output backup), with\n//! NO dead peer required.\n//!\n//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\n//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n//! a truly deadlocked one completes ~zero).\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::brain::Brain;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\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-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\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\nfn kill_pid(pid: u32) {\n    if pid == 0 {\n        return; // unknown pid — never signal pid 0 (unix process group)\n    }\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\n/// A child that FLOODS stdout forever — the heavy-output case that fills a\n/// non-draining subscriber's conn and parks its writer inside `write_frame`.\nfn flood_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\n            \"/c\".to_string(),\n            \"for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD\".to_string(),\n        ],\n    );\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\nfn connect(name: &str) -> Stream {\n    for _ in 0..200 {\n        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n            send_hello(&mut c, Role::Brain).expect(\"hello\");\n            return c;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"could not connect to broker {name}\");\n}\n\n/// Spawn one flood PTY session; return (session_id, pid, spawner_conn). The conn is\n/// kept alive (the spawner is the local by:None controller) but NOT drained — the\n/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n/// brain's viewer subscription keeps receiving live output.\nfn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n    let mut c = connect(name);\n    write_frame(\n        &mut c,\n…\n    )\n    .expect(\"send spawn\");\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,122,123,null,131]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\brain_resume_conn_deadlock.rs"}}},"isError":false,"timestamp":1784195888098}}
{"type":"message","id":"ca6060c4","parentId":"c7fbc663","timestamp":"2026-07-16T09:58:08.109Z","message":{"role":"toolResult","toolCallId":"call_BLnGGLEZzrZ9dkKpWATI7ql0|fc_07547ad47f060e5d016a58ab2f8ecc819ab70d88160c233bce","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…\n    }\n…\n}\n…\n#[test]\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n    init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let proj = dir.path().join(\"the-project-root\");\n    std::fs::create_dir_all(&proj).expect(\"project dir\");\n    let leaf = \"the-project-root\";\n\n    let name = unique_name();\n    let _broker = net_broker(&name, &dir.path().join(\"broker\"));\n    let mut brain = connect_retry(&name);\n\n    // A child that prints its cwd, then stays alive on stdin (no exit race).\n    #[cfg(unix)]\n    let (program, args) = (\n        \"sh\".to_string(),\n        vec![\"-c\".to_string(), \"pwd; cat\".to_string()],\n    );\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\"/c\".to_string(), \"cd & findstr \\\".\\\"\".to_string()],\n    );\n\n    let sid = brain\n        .spawn_session(SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: String::new(),\n            cwd: Some(proj.to_string_lossy().into_owned()),\n            env: Default::default(),\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .expect(\"spawn cwd-printing child in the project cwd\");\n\n    brain.attach(sid, 0).expect(\"attach to read output\");\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…\n            }\n…\n        }\n    }\n…\n}\n…\nfn attach_registers_remote_drive_detection() {\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\n// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n\nuse 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…\n}"}],"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…\n    }\n…\n}\n…\n#[test]\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n    init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let proj = dir.path().join(\"the-project-root\");\n    std::fs::create_dir_all(&proj).expect(\"project dir\");\n    let leaf = \"the-project-root\";\n\n    let name = unique_name();\n    let _broker = net_broker(&name, &dir.path().join(\"broker\"));\n    let mut brain = connect_retry(&name);\n\n    // A child that prints its cwd, then stays alive on stdin (no exit race).\n    #[cfg(unix)]\n    let (program, args) = (\n        \"sh\".to_string(),\n        vec![\"-c\".to_string(), \"pwd; cat\".to_string()],\n    );\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\"/c\".to_string(), \"cd & findstr \\\".\\\"\".to_string()],\n    );\n\n    let sid = brain\n        .spawn_session(SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: String::new(),\n            cwd: Some(proj.to_string_lossy().into_owned()),\n            env: Default::default(),\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .expect(\"spawn cwd-printing child in the project cwd\");\n\n    brain.attach(sid, 0).expect(\"attach to read output\");\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…\n            }\n…\n        }\n    }\n…\n}\n…\nfn attach_registers_remote_drive_detection() {\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\n// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n\nuse 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…\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,126,null,128,null,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,null,844,null,847,848,null,923,null,931,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,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,null,1155]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\attach.rs"}}},"isError":false,"timestamp":1784195888109}}
{"type":"message","id":"ebbe400f","parentId":"ca6060c4","timestamp":"2026-07-16T09:58:08.110Z","message":{"role":"toolResult","toolCallId":"call_D7cMENeekU3MWc8sXp8uvIZY|fc_07547ad47f060e5d016a58ab2f8d08819aaa2b8d687a2fe78c","toolName":"read","content":[{"type":"text","text":"//! v0.4.2 / KH 6.11 (`REQ-HAZARD-BRAIN-RESPAWN-PATH`) — the brain respawns onto\n//! the **applied bytes** after a real IN-PLACE rename, not the renamed old binary.\n//!\n//! ## Why this is a NEW test, not a tweak to `brain_survive.rs`\n//!\n//! `brain_survive.rs` models the binary swap as a **path flip** between two\n//! distinct fixtures through an injected spawn closure — it never renames a\n//! *running* binary, so it cannot exercise the production respawn resolution\n//! (`std::env::current_exe()` in the resident broker). That gap let the v0.4.1\n//! Linux brain-respawn bug ship green: on Linux `current_exe()` is\n//! `readlink(/proc/self/exe)`, **inode-tracking**, so it FOLLOWS the `apply`\n//! rename (`spt` → `spt.old-N`) and the resident broker respawned the brain onto\n//! the OLD bytes while recording `applied`. Windows `GetModuleFileName` returns\n//! the path string captured at process start, so it never followed the rename.\n//!\n//! ## The faithful reproduction\n//!\n//! This test runs the **real `spt daemon run` broker process** launched from a\n//! canonical path `P` (so the broker's own `current_exe` *is* `P`), performs the\n//! real on-disk apply choreography (**rename running `P` → `P.old-N`, write the\n//! new bytes at `P`**), then triggers a brain respawn. The respawned brain must\n//! run the bytes now at `P` (`exe_hash == B`), proving the broker respawns from\n//! the canonical path captured at start — NOT a per-spawn `current_exe()` that\n//! would, on Linux, follow the rename back to the old bytes.\n//!\n//! **Platform-honest regression guard.** PRE-fix this FAILS on Linux (the\n//! respawn lands on `P.old-N`, `exe_hash == A`) and passes on Windows (which\n//! never followed the rename). POST-fix it passes on both. So on CI it is green\n//! on both runners only because the fix is in; a regression re-arms the Linux red.\n// [int->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// `(pid, exe_hash)` out of a `brain.ready` body; `None` until it carries a pid.\nfn read_ready(path: &Path) -> Option<(u32, Option<String>)> {\n    let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()?;\n    let pid = v.get(\"pid\")?.as_u64()? as u32;\n    let exe_hash = v\n        .get(\"exe_hash\")\n        .and_then(|h| h.as_str())\n        .map(str::to_string);\n    Some((pid, exe_hash))\n}\n\n/// Poll `brain.ready` until it holds a pid different from `was`.\nfn wait_ready(path: &Path, was: Option<u32>, budget: Duration) -> Option<(u32, Option<String>)> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some((pid, hash)) = read_ready(path) {\n            if was != Some(pid) {\n                return Some((pid, hash));\n            }\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\n/// Lowercase-hex SHA-256 of a file — the digest `brain.ready.exe_hash` carries.\nfn file_hash(path: &Path) -> String {\n    use sha2::{Digest, Sha256};\n    let bytes = std::fs::read(path).expect(\"read file for hashing\");\n    let mut h = Sha256::new();\n    h.update(&bytes);\n    h.finalize().iter().map(|b| format!(\"{b:02x}\")).collect()\n}\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\n#[test]\nfn brain_respawns_onto_applied_bytes_after_in_place_rename() {\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    let bindir = home.path().join(\"bin\");\n    std::fs::create_dir_all(&bindir).expect(\"create bindir\");\n    let real = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let suffix = real\n        .extension()\n        .map(|e| format!(\".{}\", e.to_string_lossy()))\n        .unwrap_or_default();\n\n    // The canonical install path P the broker runs from — so the broker's own\n    // current_exe IS P (the resident-broker condition the bug needs).\n    let canonical = bindir.join(format!(\"spt{suffix}\"));\n    std::fs::copy(&real, &canonical).expect(\"install bytes A at canonical P\");\n    let hash_a = file_hash(&canonical);\n\n    // The REAL broker process, launched FROM P.\n    let ready = home.path().join(\"brain.ready\");\n    let mut broker: Child = Command::new(&canonical)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .spawn()\n        .expect(\"spawn the real broker from canonical P\");\n\n    // gen-0 brain came up from P (bytes A).\n    let (pid0, hash0) = wait_ready(&ready, None, Duration::from_secs(45)).unwrap_or_else(|| {\n        let _ = broker.kill();\n        panic!(\"gen-0 brain never signalled ready\");\n    });\n    assert_eq!(\n        hash0.as_deref(),\n        Some(hash_a.as_str()),\n        \"gen-0 brain runs bytes A (the canonical install)\"\n    );\n\n    // ── The real apply choreography on disk: rename the running canonical binary\n    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\n    //    what `spt update apply` does (rename-then-replace). ──\n    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\n    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\n    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\n    {\n        use std::io::Write;\n        let mut f = std::fs::OpenOptions::new()\n            .append(true)\n            .open(&canonical)\n            .expect(\"open the new P to append padding\");\n        f.write_all(&[0u8; 4096]).expect(\"append padding -> bytes B\");\n    }\n    let hash_b = file_hash(&canonical);\n    assert_ne!(hash_a, hash_b, \"the swap must flip the binary hash\");\n\n    // ── Trigger a brain respawn (kill the brain; the supervisor respawns it). The\n    //    respawn must land on the APPLIED bytes now at P (the canonical path the\n    //    broker captured at start), NOT the renamed P.old-8. Pre-fix on Linux the\n    //    broker's per-spawn current_exe() (readlink /proc/self/exe) follows the\n    //    rename to P.old-8 and respawns bytes A → this assert fails; post-fix it\n    //    spawns the captured P (bytes B). Windows never followed the rename. ──\n    kill_pid(pid0);\n    let (pid1, hash1) = wait_ready(&ready, Some(pid0), Duration::from_secs(45)).unwrap_or_else(|| {\n        kill_pid(pid0);\n        let _ = broker.kill();\n        panic!(\"supervisor never respawned the brain after the rename\");\n    });\n    assert_ne!(pid0, pid1, \"the respawned brain is a fresh process\");\n    assert_eq!(\n        hash1.as_deref(),\n        Some(hash_b.as_str()),\n        \"post-rename respawn must run the APPLIED bytes at the canonical path (B), \\\n         not the renamed old binary (A) — KH 6.11 / REQ-HAZARD-BRAIN-RESPAWN-PATH\"\n    );\n\n    // Teardown.\n    kill_pid(pid1);\n    let _ = broker.kill();\n    let _ = broker.wait();\n}\n"}],"details":{"displayContent":{"text":"//! v0.4.2 / KH 6.11 (`REQ-HAZARD-BRAIN-RESPAWN-PATH`) — the brain respawns onto\r\n//! the **applied bytes** after a real IN-PLACE rename, not the renamed old binary.\r\n//!\r\n//! ## Why this is a NEW test, not a tweak to `brain_survive.rs`\r\n//!\r\n//! `brain_survive.rs` models the binary swap as a **path flip** between two\r\n//! distinct fixtures through an injected spawn closure — it never renames a\r\n//! *running* binary, so it cannot exercise the production respawn resolution\r\n//! (`std::env::current_exe()` in the resident broker). That gap let the v0.4.1\r\n//! Linux brain-respawn bug ship green: on Linux `current_exe()` is\r\n//! `readlink(/proc/self/exe)`, **inode-tracking**, so it FOLLOWS the `apply`\r\n//! rename (`spt` → `spt.old-N`) and the resident broker respawned the brain onto\r\n//! the OLD bytes while recording `applied`. Windows `GetModuleFileName` returns\r\n//! the path string captured at process start, so it never followed the rename.\r\n//!\r\n//! ## The faithful reproduction\r\n//!\r\n//! This test runs the **real `spt daemon run` broker process** launched from a\r\n//! canonical path `P` (so the broker's own `current_exe` *is* `P`), performs the\r\n//! real on-disk apply choreography (**rename running `P` → `P.old-N`, write the\r\n//! new bytes at `P`**), then triggers a brain respawn. The respawned brain must\r\n//! run the bytes now at `P` (`exe_hash == B`), proving the broker respawns from\r\n//! the canonical path captured at start — NOT a per-spawn `current_exe()` that\r\n//! would, on Linux, follow the rename back to the old bytes.\r\n//!\r\n//! **Platform-honest regression guard.** PRE-fix this FAILS on Linux (the\r\n//! respawn lands on `P.old-N`, `exe_hash == A`) and passes on Windows (which\r\n//! never followed the rename). POST-fix it passes on both. So on CI it is green\r\n//! on both runners only because the fix is in; a regression re-arms the Linux red.\r\n// [int->REQ-HAZARD-BRAIN-RESPAWN-PATH]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::process::{Child, Command};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\n/// `(pid, exe_hash)` out of a `brain.ready` body; `None` until it carries a pid.\r\nfn read_ready(path: &Path) -> Option<(u32, Option<String>)> {\r\n    let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()?;\r\n    let pid = v.get(\"pid\")?.as_u64()? as u32;\r\n    let exe_hash = v\r\n        .get(\"exe_hash\")\r\n        .and_then(|h| h.as_str())\r\n        .map(str::to_string);\r\n    Some((pid, exe_hash))\r\n}\r\n\r\n/// Poll `brain.ready` until it holds a pid different from `was`.\r\nfn wait_ready(path: &Path, was: Option<u32>, budget: Duration) -> Option<(u32, Option<String>)> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some((pid, hash)) = read_ready(path) {\r\n            if was != Some(pid) {\r\n                return Some((pid, hash));\r\n            }\r\n        }\r\n        thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\n/// Lowercase-hex SHA-256 of a file — the digest `brain.ready.exe_hash` carries.\r\nfn file_hash(path: &Path) -> String {\r\n    use sha2::{Digest, Sha256};\r\n    let bytes = std::fs::read(path).expect(\"read file for hashing\");\r\n    let mut h = Sha256::new();\r\n    h.update(&bytes);\r\n    h.finalize().iter().map(|b| format!(\"{b:02x}\")).collect()\r\n}\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\n#[test]\r\nfn brain_respawns_onto_applied_bytes_after_in_place_rename() {\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    let bindir = home.path().join(\"bin\");\r\n    std::fs::create_dir_all(&bindir).expect(\"create bindir\");\r\n    let real = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n    let suffix = real\r\n        .extension()\r\n        .map(|e| format!(\".{}\", e.to_string_lossy()))\r\n        .unwrap_or_default();\r\n\r\n    // The canonical install path P the broker runs from — so the broker's own\r\n    // current_exe IS P (the resident-broker condition the bug needs).\r\n    let canonical = bindir.join(format!(\"spt{suffix}\"));\r\n    std::fs::copy(&real, &canonical).expect(\"install bytes A at canonical P\");\r\n    let hash_a = file_hash(&canonical);\r\n\r\n    // The REAL broker process, launched FROM P.\r\n    let ready = home.path().join(\"brain.ready\");\r\n    let mut broker: Child = Command::new(&canonical)\r\n        .no_window()\r\n        .args([\"daemon\", \"run\"])\r\n        .env(\"SPT_HOME\", home.path())\r\n        .spawn()\r\n        .expect(\"spawn the real broker from canonical P\");\r\n\r\n    // gen-0 brain came up from P (bytes A).\r\n    let (pid0, hash0) = wait_ready(&ready, None, Duration::from_secs(45)).unwrap_or_else(|| {\r\n        let _ = broker.kill();\r\n        panic!(\"gen-0 brain never signalled ready\");\r\n    });\r\n    assert_eq!(\r\n        hash0.as_deref(),\r\n        Some(hash_a.as_str()),\r\n        \"gen-0 brain runs bytes A (the canonical install)\"\r\n    );\r\n\r\n    // ── The real apply choreography on disk: rename the running canonical binary\r\n    //    P -> P.old-8, then write the NEW bytes (B = A + padding) at P. Exactly\r\n    //    what `spt update apply` does (rename-then-replace). ──\r\n    let old_n = bindir.join(format!(\"spt.old-8{suffix}\"));\r\n    std::fs::rename(&canonical, &old_n).expect(\"rename running P -> P.old-8 (the apply rename)\");\r\n    std::fs::copy(&real, &canonical).expect(\"write fresh bytes at P\");\r\n    {\r\n        use std::io::Write;\r\n        let mut f = std::fs::OpenOptions::new()\r\n            .append(true)\r\n            .open(&canonical)\r\n            .expect(\"open the new P to append padding\");\r\n        f.write_all(&[0u8; 4096]).expect(\"append padding -> bytes B\");\r\n    }\r\n    let hash_b = file_hash(&canonical);\r\n    assert_ne!(hash_a, hash_b, \"the swap must flip the binary hash\");\r\n\r\n    // ── Trigger a brain respawn (kill the brain; the supervisor respawns it). The\r\n    //    respawn must land on the APPLIED bytes now at P (the canonical path the\r\n    //    broker captured at start), NOT the renamed P.old-8. Pre-fix on Linux the\r\n    //    broker's per-spawn current_exe() (readlink /proc/self/exe) follows the\r\n    //    rename to P.old-8 and respawns bytes A → this assert fails; post-fix it\r\n    //    spawns the captured P (bytes B). Windows never followed the rename. ──\r\n    kill_pid(pid0);\r\n    let (pid1, hash1) = wait_ready(&ready, Some(pid0), Duration::from_secs(45)).unwrap_or_else(|| {\r\n        kill_pid(pid0);\r\n        let _ = broker.kill();\r\n        panic!(\"supervisor never respawned the brain after the rename\");\r\n    });\r\n    assert_ne!(pid0, pid1, \"the respawned brain is a fresh process\");\r\n    assert_eq!(\r\n        hash1.as_deref(),\r\n        Some(hash_b.as_str()),\r\n        \"post-rename respawn must run the APPLIED bytes at the canonical path (B), \\\r\n         not the renamed old binary (A) — KH 6.11 / REQ-HAZARD-BRAIN-RESPAWN-PATH\"\r\n    );\r\n\r\n    // Teardown.\r\n    kill_pid(pid1);\r\n    let _ = broker.kill();\r\n    let _ = broker.wait();\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\brain_respawn_rename.rs"}}},"isError":false,"timestamp":1784195888110}}
{"type":"message","id":"29812cea","parentId":"ebbe400f","timestamp":"2026-07-16T09:58:19.065Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning mixed-session test scenario**\n\n**Designing minimal multi-controller test setup**"},{"type":"toolCall","id":"call_LbhPqFHz7htTJpHzbiNQJi2q|fc_07547ad47f060e5d016a58ab3a5e6c819a96b90ea8cab74476","name":"read","arguments":{"i":"Reading mock dummy main loop","path":"adapters/mock/src/main.rs","selector":"135-191"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":17852,"output":339,"cacheRead":44544,"cacheWrite":0,"totalTokens":62735,"reasoningTokens":301,"cost":{"input":0.08926,"output":0.01017,"cacheRead":0.022272,"cacheWrite":0,"total":0.121702}},"stopReason":"toolUse","timestamp":1784195888112,"responseId":"resp_07547ad47f060e5d016a58ab309af0819ab4cace9a4015acfa","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab33954c819aa7734be36cec68f4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKs6cXUFxO_9TwIjrxN0J7IL_MjXMr9bAcJU5t3ZiWPyRNtbAtLKiwqhQZ284vc11c8F6o1syS_lnfoGoRrbuRc81V5BoqrRfsk3osbXG1qRLGlOViLFb5zeq317vxMeArCGfC9caeGY6U69TWalEwyAbbe_dzREZif9ZVV3WIY0DcIHxYj43dEFI0hIM0mfOrLeLp_R-mEKnsTv-N4PJC-zxkGC20LiPcEkOlNo0-wYmEdhoNcXdH95ZYVpft8X-pC3p5zsilya8goOXia9mQd4BMDBXPZP_krj1k1fKKRnAdWD0-0t3b75JGBkLcIuUOwaLP5lu2cqC1eYvkKFAfapEYHpW2Ywat0IgQfMEax7mT1uFCW-vSHqPAZprC3qSrFte9paX9HCEQiAEdEfcIt4fHvcPbhOAcJ29-jZqtTlRY01j8_DOI6jlruZ2LkL2TgX2oq9R8aV2gOv39Vz4hSnhOTYrFXkk7GEj9gJmubqkoY4xMdu3jZ4ySPvfDlTpPU8faAE1lGj-IdP4kAPXfaNpAAYPQ_0sBCpFPjW61JOo5tap7wm8FPwT6ss8fhNKahu8yoDSTc3Q-jObALhpXM39M3CEAMkP1T1p4EUzYUqR_tFo-qSWhsIXZENdF36P3lHuuAQRya2dVXVSMdF2lrZ0TkJQaq2k1i9_0xOa49y-he8C56B_OMvdD8XFKxARDn5nLzJBd2CNIBtBa7gynjC6s-E_8-pq3D1-XtdTiP41uhGAHqasxRyfkdDSp-uDXPIrw_sjZ5jToBMVI01s558E7jr_KHTE4o1auEJdmQrzcxkquf9Mxb_G0o2-5OJTrJcFq1suU_4M-xuM_CyvPkQdq6MfYgrqdD2B9A3wVuSj16jTSA-Gd2PV37mYXllztMx6XHeg_DQCtzu5uOVTxFIHpe3jKWVvs2vcfAb9_KwmzLJolBG2VsNG5EQp9Vq8Cp-LtMrBEjFrAUeA-hm6F_UB44_WJoEu69wwpGchygOpYG7yvedRragHUvBvezdMqw5AWVFr0mURWX-7tbUFVzXI0RTGdjTk0bs55oYnc_zYOX4JEcPmoCrCV9LYopcMPgOwrwreT1JkI8TxgHT-MoDKEX8j29OmDCcgNKDvp94RW0gDEJIcx1GYeC_yYtJLMK14VamM17hfSHs9CMxci5aobLwmgS0SCtxRt68nCWQVVHYdE78Pbc7TFDfYR0Tnm44BXGPo1u7u2VNMRrBDrVTKEWB1hzaupOQqcRAoEXAgv365y0oXq62Wpr3BqlbvN35NvsOXun0b6H2CT4_IpMJk23AUman82ughrNufp6JUKnJji1D0ax4G2XbvXHrvi_VcHrblm75RvubnilNQojwnwcjtbEwXunyhE232UfqsazS71zeUpfbJqOfs_xFNflI8EljaXTIvpnv1mxGt1W05C4lJ9czfrpF8S0-Lxmam6bDBkSPptdPF9fbLOruiyWoyme_ei-Xb57ReTQjbbKd0PIhStZ8Bb8lHwyjZVYJKRdnTMVYv3HkPnElnYopjLC4EGSwUSLLUs___db7ATgRuN001qAOyA4fuwWMhK4Jl4IfmAUBtHhSQp51Lg0yrrGGTA2oG9CJLpuNs7VlH_z95vlg8Ftx55SDRS7BEizoYt4yVgYWwdHFFbEIfYFC97xaWNhjt9q6iYM35e_X3e38oo0rASKK1O0Io77F7hBPepQ684Ko0BujqBS6JP0KIV4I8vlLcESBPuavVdSgM24CEufcyp0T8m6YN6aMcJ3Ms5OxcNSWf9BB1zbyWn9QMe5sGRPRtr2KEvtYlyUPAmTzMYn1p1W51WBPzkl3x2Y5JMLT1puSR8Jx8KdwqKqTd-wTc9VgOwtSeEyq1o17kdBy1OsQAnr3nc952Kk9LcZ8pKAZ1FfwS9DxBvrZZrZMopnxsfzCrDt8dHBlx8wmakYwqut8PO9bH8HDFRx6QnDKaLRKBXAQxPVbrGsbGyvzQfVxxBEgt8asWJQDgGrT69tgwtVlcPbN2ORKytXk3Tp-UOBUvDhBMHSBpPM-cIq7FaziKxDs9FTzNWOr6UueyHPadAgMqJJOOHB4M7jyt08WFc-EeGxbleqRZOAkyveOC3oYHs6OMNzNAjM8BM6fnlElK2cOcWHL-TkrHwfUys2IsOKgHfRRCZhl-Q6EYliTo7YlylQqIP8Y4TXqKnxwGJYNxqqgBpl1xxNzO7TlUEToMlDSFB5qdesv5n2Sf0guv01m0IjXdxcyctKG-Q0SxNrg7Q3QquKxRKFEofg5pE7IRU5gIrs-eWSJzNW5PSAClKaoX-ija3rNrEwPTVSpK1bSL5il5w69jfpfD_KGZ6ktE1DmgDHCcl-NnxX4kGCKabuvdnNxJIhvTgA5r9jJwe8ijnTrnh7r7jUlz1q4sFMTNN0OBw_mMen8ko7Kmk6N8Yhd4O4Ny_xQYpd6eKB6cdFOkLFn9wAV_P1HsXC_X-rLy9hRj0gV5FJcJ6rlcy5dTRLRLGqxLetQ5aqjBBJH6m6hqwgpQhiSPboexFJ3pDDHVgfSVzPdZ2rTsBhzVr4hp2qlwwxiO71DiuBIm_t66kFhF-fx5P85fqilUth3IVSwHYviB6nGyrNcxhF67yAlH6vYTxNQY-bALBOPB8rv8GRI3hT3Yde4GGV4Tf_ZRTcm1V76sIDp-xaLXUMd7_55M98oRi8TfpJd6dX6PHAPSJiUkY7_XtbEf7Zh9kycVu1A56gq1B9hqqIHVOZ-RIHEachP","summary":[{"type":"summary_text","text":"**Planning mixed-session test scenario**"},{"type":"summary_text","text":"**Designing minimal multi-controller test setup**"}]},{"id":"fc_07547ad47f060e5d016a58ab3a5e6c819a96b90ea8cab74476","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading mock dummy main loop\",\"path\":\"adapters/mock/src/main.rs\",\"selector\":\"135-191\"}","call_id":"call_LbhPqFHz7htTJpHzbiNQJi2q","name":"read"}]},"duration":10951.944000000134,"ttft":3447.9479000000283,"contextSnapshot":{"promptTokens":62396,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_LbhPqFHz7htTJpHzbiNQJi2q|fc_07547ad47f060e5d016a58ab3a5e6c819a96b90ea8cab74476","toolName":"read","startedAt":"2026-07-16T09:58:19.065Z","args":{"path":"adapters/mock/src/main.rs"},"intent":"Reading mock dummy main loop"},"id":"9d40f34e","parentId":"29812cea","timestamp":"2026-07-16T09:58:19.065Z"}
{"type":"message","id":"0ef0b7b7","parentId":"9d40f34e","timestamp":"2026-07-16T09:58:19.071Z","message":{"role":"toolResult","toolCallId":"call_LbhPqFHz7htTJpHzbiNQJi2q|fc_07547ad47f060e5d016a58ab3a5e6c819a96b90ea8cab74476","toolName":"read","content":[{"type":"text","text":"fn main() {\n…\n    match arg(&args, \"--mode\").as_deref() {\n…\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\n…\n        }\n        _ => {} // default: the agent-session stand-in below.\n    }\n\n    let id = arg(&args, \"--id\").unwrap_or_else(|| {\n        eprintln!(\"mock-session: --id required\");\n        std::process::exit(2);\n    });\n    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n    let spt = arg(&args, \"--spt\")\n        .or_else(|| std::env::var(\"SPT_BIN\").ok())\n        .unwrap_or_else(|| \"spt\".to_string());\n    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.\n    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.\n    let endpoint_type = arg(&args, \"--type\");\n    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n\n    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\n    let mut bind_args = vec![\n        \"api\".to_string(),\n        \"--adapter\".to_string(),\n        adapter.clone(),\n        \"bind\".to_string(),\n        id.clone(),\n        \"--set-session-id\".to_string(),\n        session_id.clone(),\n    ];\n    if let Some(ty) = &endpoint_type {\n        bind_args.push(\"--type\".to_string());\n        bind_args.push(ty.clone());\n    }\n    let bind = Command::new(&spt).args(&bind_args).status();\n    if !matches!(bind, Ok(s) if s.success()) {\n        eprintln!(\"mock-session: api bind failed: {bind:?}\");\n        std::process::exit(1);\n    }\n\n    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.\n    let state = Command::new(&spt)\n        .args([\n            \"api\",\n            \"--adapter\",\n            &adapter,\n            \"state\",\n            \"idle\",\n            &id,\n            \"--session-id\",\n            &session_id,\n        ])\n        .status();\n    if !matches!(state, Ok(s) if s.success()) {\n        eprintln!(\"mock-session: api state idle failed: {state:?}\");\n        std::process::exit(1);\n    }\n\n    println!(\"MOCK_SESSION_READY:{id}\");\n}\n"}],"details":{"displayContent":{"text":"fn main() {\n…\n    match arg(&args, \"--mode\").as_deref() {\n…\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\n…\n        }\r\n        _ => {} // default: the agent-session stand-in below.\r\n    }\r\n\r\n    let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n        eprintln!(\"mock-session: --id required\");\r\n        std::process::exit(2);\r\n    });\r\n    let session_id = arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n    let spt = arg(&args, \"--spt\")\r\n        .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n        .unwrap_or_else(|| \"spt\".to_string());\r\n    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.\r\n    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.\r\n    let endpoint_type = arg(&args, \"--type\");\r\n    let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n\r\n    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.\r\n    let mut bind_args = vec![\r\n        \"api\".to_string(),\r\n        \"--adapter\".to_string(),\r\n        adapter.clone(),\r\n        \"bind\".to_string(),\r\n        id.clone(),\r\n        \"--set-session-id\".to_string(),\r\n        session_id.clone(),\r\n    ];\r\n    if let Some(ty) = &endpoint_type {\r\n        bind_args.push(\"--type\".to_string());\r\n        bind_args.push(ty.clone());\r\n    }\r\n    let bind = Command::new(&spt).args(&bind_args).status();\r\n    if !matches!(bind, Ok(s) if s.success()) {\r\n        eprintln!(\"mock-session: api bind failed: {bind:?}\");\r\n        std::process::exit(1);\r\n    }\r\n\r\n    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.\r\n    let state = Command::new(&spt)\r\n        .args([\r\n            \"api\",\r\n            \"--adapter\",\r\n            &adapter,\r\n            \"state\",\r\n            \"idle\",\r\n            &id,\r\n            \"--session-id\",\r\n            &session_id,\r\n        ])\r\n        .status();\r\n    if !matches!(state, Ok(s) if s.success()) {\r\n        eprintln!(\"mock-session: api state idle failed: {state:?}\");\r\n        std::process::exit(1);\r\n    }\r\n\r\n    println!(\"MOCK_SESSION_READY:{id}\");\r\n}\r\n","startLine":20,"lineNumbers":[20,null,26,null,40,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\adapters\\mock\\src\\main.rs"}}},"isError":false,"timestamp":1784195899071}}
{"type":"message","id":"a678b2e8","parentId":"0ef0b7b7","timestamp":"2026-07-16T09:58:24.546Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_g5Bb9SANAfwVEKjsj1aY9ABH|fc_07547ad47f060e5d016a58ab4069b0819a9e807441a2f9a7c1","name":"read","arguments":{"i":"Reading dummy heartbeat loop","path":"adapters/mock/src/main.rs","selector":"25-90"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4394,"output":47,"cacheRead":58880,"cacheWrite":0,"totalTokens":63321,"reasoningTokens":10,"cost":{"input":0.021970000000000003,"output":0.00141,"cacheRead":0.029439999999999997,"cacheWrite":0,"total":0.052820000000000006}},"stopReason":"toolUse","timestamp":1784195899082,"responseId":"resp_07547ad47f060e5d016a58ab3b928c819a9e3d643f8a1f1217","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab3e456c819a803fd67f6a6fccca","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtAbGDAr0BVI5iQELZceLRqMJNNXUBXnJaWoQCbtatgqsNSqpO0y_sP6XZMeWhGkIfiFuh8kCCtvlGOxGx4xQi0gG-8pEY_p4gAMclW7_UoyVkCyg-J8bqHa8Q_vwrQ0Sokkuh7oRSOyzvCzDQQcKk7ftN5G9iJsAbBzGmnV3a_ys6GBkP2TUOlYAstvZX5aP7bDXeMzwNee6SxwH4glZtBQWlPdnYFQuy2m-rjtrqKBlUUPrKrU3uGQ05p_b3Jt1cuujr5Uw8tjfxUS2Q9W3rkp-kEKPPX-ETIL53U5nN-VURIYBbH_WErX4jmK2cO4g03TGG6ot02loJCTQR8DEBIYinEoASkljNC4qmpgQWXDElKthNQoa3rXQWyBLrTYVKpG-_yl9G3xOuyR43uNGl8yA44VE1tkpA87zO80JNOKYgBpqgSmJZX2JxLqAmTqnw9ektXYEG8pR6rHT4jTMVzFwytbou4HYONy0jeQqC55b8YhedFaoCPR1ADsRrEFNih_qd5lNoC1TpVxueYSK7ubFs5BRWLHQbczt-Mxxp8fCsRWzLpMM84LUMUiyvBer_UtoTP23M5OgX7hZ_FrHLm0W-zjUVxqS_J0D5wmXmGDfyDeym77x_nAHwubuOYfxy-RqDBnINvT3btV2W0sVjh40PS7QWEIrqzGD1jpeuQd9WGrbY3W2g7983OqkafmpfhFMWaTxOojCfDTwCCnIT8vDX-fwdqhfWdRBZd78gG20OTQsSvZMKMZA9C9ESHBIEZmw1B02tI5EKCrzHshgTdbA-xiaoBbSFc4PZxGlIEgkBqNqWiMkY8uTCoKW1aUDw-0uILTnKKzFNkE7wBfTpGLld1Z8NnvnK5nttg5oxo5CTMvjPJ5fCh_ckwKq-E-dhUNyVPehtD4jLyKWIm9bWZ9qweWFMv67Bsz6_yKf1XlVSo0ZZl8yjCopqCF8ku6Ths52qmTB3Uwpv8JoAR1jlHvA==","summary":[]},{"id":"fc_07547ad47f060e5d016a58ab4069b0819a9e807441a2f9a7c1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dummy heartbeat loop\",\"path\":\"adapters/mock/src/main.rs\",\"selector\":\"25-90\"}","call_id":"call_g5Bb9SANAfwVEKjsj1aY9ABH","name":"read"}]},"duration":5460.109299999662,"ttft":3166.621799999848,"contextSnapshot":{"promptTokens":63274,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_g5Bb9SANAfwVEKjsj1aY9ABH|fc_07547ad47f060e5d016a58ab4069b0819a9e807441a2f9a7c1","toolName":"read","startedAt":"2026-07-16T09:58:24.546Z","args":{"path":"adapters/mock/src/main.rs"},"intent":"Reading dummy heartbeat loop"},"id":"8b8d55de","parentId":"a678b2e8","timestamp":"2026-07-16T09:58:24.546Z"}
{"type":"message","id":"385bd6e0","parentId":"8b8d55de","timestamp":"2026-07-16T09:58:24.557Z","message":{"role":"toolResult","toolCallId":"call_g5Bb9SANAfwVEKjsj1aY9ABH|fc_07547ad47f060e5d016a58ab4069b0819a9e807441a2f9a7c1","toolName":"read","content":[{"type":"text","text":"    // binds its nested perch; the child just needs to launch). `echo` is the\n    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.\n    match arg(&args, \"--mode\").as_deref() {\n        Some(\"psyche\") => {\n            let id = arg(&args, \"--id\").unwrap_or_else(|| \"mock-psyche\".to_string());\n            println!(\"MOCK_PSYCHE_UP:{id}\");\n            return;\n        }\n        Some(\"echo\") => {\n            use std::io::Read;\n            let mut history = String::new();\n            let _ = std::io::stdin().read_to_string(&mut history);\n            // A deterministic brief; a real echo summarizes the history it read.\n            println!(\"mock echo brief ({} history bytes)\", history.len());\n            return;\n        }\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\n            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived\n            // stand-in for a real harness session. Binds its perch (the harness\n            // contract) exactly like the default stand-in, then prints a stdout\n            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL\n            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`\n            // attach path can be exercised end-to-end (the default mode exits\n            // immediately, which is why the v0.12.0 mock tests never caught the\n            // daemon+broker+PTY lifecycle defects). Isolates \"broker wedged /\n            // endpoint died\" from \"a real harness failed to launch.\"\n            //\n            // `--mode hold-unbound` (W6, REQ-ENDPOINT-UNBOUND-ATTACH): identical, but\n            // it NEVER binds — the broker session is live (PTY + heartbeat) while the\n            // perch stays the endpoint-run UNBOUND skeleton forever. That gives the\n            // int a deterministic, race-free live pre-bind session to prove the\n            // attach-before-bind contract (`spt rc` attaches a STATUS_UNBOUND\n            // endpoint) + the real picker Unbound render against a REAL session. The\n            // UNBOUND→ONLINE flip is proven separately by the normal `dummy` bringup.\n            let no_bind = mode == \"hold-unbound\";\n            use std::io::Write;\n            let id = arg(&args, \"--id\").unwrap_or_else(|| {\n                eprintln!(\"mock-session --mode dummy: --id required\");\n                std::process::exit(2);\n            });\n            let session_id =\n                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\n            let spt = arg(&args, \"--spt\")\n                .or_else(|| std::env::var(\"SPT_BIN\").ok())\n                .unwrap_or_else(|| \"spt\".to_string());\n            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\n            let endpoint_type = arg(&args, \"--type\");\n            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\n                .and_then(|s| s.parse().ok())\n                .unwrap_or(500);\n\n            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when\n            // `--marker-file <path>` is set, write a marker whose CONTENTS record\n            // (a) which session template launched this dummy — `resume` if the\n            // command carried `--resume-marker`, else `self` — and (b) the\n            // `current_dir()` the harness landed in. The int test declares a\n            // `[session.self]` with a SELF marker path and a `[session.resume]` with\n            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a\n            // `--resume` bringup proves both the template SELECTION and the cwd\n            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.\n            if let Some(marker) = arg(&args, \"--marker-file\") {\n                let which = if args.iter().any(|a| a == \"--resume-marker\") {\n                    \"resume\"\n                } else {\n                    \"self\"\n                };\n                let cwd = std::env::current_dir()\n                    .map(|p| p.display().to_string())\n                    .unwrap_or_else(|_| \"<no-cwd>\".to_string());\n                // `template=<self|resume>\\ncwd=<dir>\\n` — a parseable two-line proof.\n…\n            }\n…\n        }\n…\n    }\n\n[Showing lines 24-93 of 191. Use :94 to continue]"}],"details":{"truncation":{"content":"    // binds its nested perch; the child just needs to launch). `echo` is the\r\n    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.\r\n    match arg(&args, \"--mode\").as_deref() {\r\n        Some(\"psyche\") => {\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| \"mock-psyche\".to_string());\r\n            println!(\"MOCK_PSYCHE_UP:{id}\");\r\n            return;\r\n        }\r\n        Some(\"echo\") => {\r\n            use std::io::Read;\r\n            let mut history = String::new();\r\n            let _ = std::io::stdin().read_to_string(&mut history);\r\n            // A deterministic brief; a real echo summarizes the history it read.\r\n            println!(\"mock echo brief ({} history bytes)\", history.len());\r\n            return;\r\n        }\r\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\r\n            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived\r\n            // stand-in for a real harness session. Binds its perch (the harness\r\n            // contract) exactly like the default stand-in, then prints a stdout\r\n            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL\r\n            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`\r\n            // attach path can be exercised end-to-end (the default mode exits\r\n            // immediately, which is why the v0.12.0 mock tests never caught the\r\n            // daemon+broker+PTY lifecycle defects). Isolates \"broker wedged /\r\n            // endpoint died\" from \"a real harness failed to launch.\"\r\n            //\r\n            // `--mode hold-unbound` (W6, REQ-ENDPOINT-UNBOUND-ATTACH): identical, but\r\n            // it NEVER binds — the broker session is live (PTY + heartbeat) while the\r\n            // perch stays the endpoint-run UNBOUND skeleton forever. That gives the\r\n            // int a deterministic, race-free live pre-bind session to prove the\r\n            // attach-before-bind contract (`spt rc` attaches a STATUS_UNBOUND\r\n            // endpoint) + the real picker Unbound render against a REAL session. The\r\n            // UNBOUND→ONLINE flip is proven separately by the normal `dummy` bringup.\r\n            let no_bind = mode == \"hold-unbound\";\r\n            use std::io::Write;\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n                eprintln!(\"mock-session --mode dummy: --id required\");\r\n                std::process::exit(2);\r\n            });\r\n            let session_id =\r\n                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n            let spt = arg(&args, \"--spt\")\r\n                .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n                .unwrap_or_else(|| \"spt\".to_string());\r\n            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n            let endpoint_type = arg(&args, \"--type\");\r\n            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\r\n                .and_then(|s| s.parse().ok())\r\n                .unwrap_or(500);\r\n\r\n            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when\r\n            // `--marker-file <path>` is set, write a marker whose CONTENTS record\r\n            // (a) which session template launched this dummy — `resume` if the\r\n            // command carried `--resume-marker`, else `self` — and (b) the\r\n            // `current_dir()` the harness landed in. The int test declares a\r\n            // `[session.self]` with a SELF marker path and a `[session.resume]` with\r\n            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a\r\n            // `--resume` bringup proves both the template SELECTION and the cwd\r\n            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.\r\n            if let Some(marker) = arg(&args, \"--marker-file\") {\r\n                let which = if args.iter().any(|a| a == \"--resume-marker\") {\r\n                    \"resume\"\r\n                } else {\r\n                    \"self\"\r\n                };\r\n                let cwd = std::env::current_dir()\r\n                    .map(|p| p.display().to_string())\r\n                    .unwrap_or_else(|_| \"<no-cwd>\".to_string());\r\n                // `template=<self|resume>\\ncwd=<dir>\\n` — a parseable two-line proof.\r","truncated":true,"truncatedBy":"lines","totalLines":168,"totalBytes":4296,"outputLines":70,"outputBytes":4296,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    // binds its nested perch; the child just needs to launch). `echo` is the\r\n    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.\r\n    match arg(&args, \"--mode\").as_deref() {\r\n        Some(\"psyche\") => {\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| \"mock-psyche\".to_string());\r\n            println!(\"MOCK_PSYCHE_UP:{id}\");\r\n            return;\r\n        }\r\n        Some(\"echo\") => {\r\n            use std::io::Read;\r\n            let mut history = String::new();\r\n            let _ = std::io::stdin().read_to_string(&mut history);\r\n            // A deterministic brief; a real echo summarizes the history it read.\r\n            println!(\"mock echo brief ({} history bytes)\", history.len());\r\n            return;\r\n        }\r\n        Some(mode @ (\"dummy\" | \"hold-unbound\")) => {\r\n            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived\r\n            // stand-in for a real harness session. Binds its perch (the harness\r\n            // contract) exactly like the default stand-in, then prints a stdout\r\n            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL\r\n            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`\r\n            // attach path can be exercised end-to-end (the default mode exits\r\n            // immediately, which is why the v0.12.0 mock tests never caught the\r\n            // daemon+broker+PTY lifecycle defects). Isolates \"broker wedged /\r\n            // endpoint died\" from \"a real harness failed to launch.\"\r\n            //\r\n            // `--mode hold-unbound` (W6, REQ-ENDPOINT-UNBOUND-ATTACH): identical, but\r\n            // it NEVER binds — the broker session is live (PTY + heartbeat) while the\r\n            // perch stays the endpoint-run UNBOUND skeleton forever. That gives the\r\n            // int a deterministic, race-free live pre-bind session to prove the\r\n            // attach-before-bind contract (`spt rc` attaches a STATUS_UNBOUND\r\n            // endpoint) + the real picker Unbound render against a REAL session. The\r\n            // UNBOUND→ONLINE flip is proven separately by the normal `dummy` bringup.\r\n            let no_bind = mode == \"hold-unbound\";\r\n            use std::io::Write;\r\n            let id = arg(&args, \"--id\").unwrap_or_else(|| {\r\n                eprintln!(\"mock-session --mode dummy: --id required\");\r\n                std::process::exit(2);\r\n            });\r\n            let session_id =\r\n                arg(&args, \"--session-id\").unwrap_or_else(|| \"mock-session-1\".to_string());\r\n            let spt = arg(&args, \"--spt\")\r\n                .or_else(|| std::env::var(\"SPT_BIN\").ok())\r\n                .unwrap_or_else(|| \"spt\".to_string());\r\n            let adapter = arg(&args, \"--mock-adapter\").unwrap_or_else(|| \"mock\".to_string());\r\n            let endpoint_type = arg(&args, \"--type\");\r\n            let heartbeat_ms: u64 = arg(&args, \"--heartbeat-ms\")\r\n                .and_then(|s| s.parse().ok())\r\n                .unwrap_or(500);\r\n\r\n            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when\r\n            // `--marker-file <path>` is set, write a marker whose CONTENTS record\r\n            // (a) which session template launched this dummy — `resume` if the\r\n            // command carried `--resume-marker`, else `self` — and (b) the\r\n            // `current_dir()` the harness landed in. The int test declares a\r\n            // `[session.self]` with a SELF marker path and a `[session.resume]` with\r\n            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a\r\n            // `--resume` bringup proves both the template SELECTION and the cwd\r\n            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.\r\n            if let Some(marker) = arg(&args, \"--marker-file\") {\r\n                let which = if args.iter().any(|a| a == \"--resume-marker\") {\r\n                    \"resume\"\r\n                } else {\r\n                    \"self\"\r\n                };\r\n                let cwd = std::env::current_dir()\r\n                    .map(|p| p.display().to_string())\r\n                    .unwrap_or_else(|_| \"<no-cwd>\".to_string());\r\n                // `template=<self|resume>\\ncwd=<dir>\\n` — a parseable two-line proof.\r\n…\n            }\n…\n        }\n…\n    }","startLine":24,"lineNumbers":[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,null,95,null,134,null,136]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\adapters\\mock\\src\\main.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":191,"totalBytes":4296,"outputLines":70,"outputBytes":4296,"shownRange":{"start":24,"end":93},"nextOffset":94}}},"isError":false,"timestamp":1784195904557}}
{"type":"message","id":"01a774c3","parentId":"385bd6e0","timestamp":"2026-07-16T09:58:24.558Z","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":"e323ab6d","parentId":"01a774c3","timestamp":"2026-07-16T09:58:37.743Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating remote brain connection handling**\n\n**Inspecting brain process run loop**"},{"type":"toolCall","id":"call_KiuKZ3vkKXVhgzXIOVtQnqfS|fc_07547ad47f060e5d016a58ab4a8e7c819abc902541eacdd4c7","name":"irc","arguments":{"i":"Acknowledging hypothesis check","op":"send","to":"Main","message":"Checking exact remote rc dispatch/serve_attach ownership now. Key correction so far: refresh brain boot uses cursor-only resume_session_cursors (no session subscription), so existing remote attach streams need explicit redispatch/re-serve; determining whether stream worker gets its own broker conn or dies with old brain.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4606,"output":262,"cacheRead":59904,"cacheWrite":0,"totalTokens":64772,"reasoningTokens":147,"cost":{"input":0.023030000000000002,"output":0.00786,"cacheRead":0.029952,"cacheWrite":0,"total":0.060842}},"stopReason":"toolUse","timestamp":1784195904563,"responseId":"resp_07547ad47f060e5d016a58ab4110dc819a8930e92cdada139a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab44bcd8819a8748c5740c2d0108","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtKE7yfyWnxpfqd49LhHa1dnW9MBeVHxPIa41_DxTdozHmlDi-ldS7DkuErT4fuRwzp0dG9bzBm1yj5RnRgAjTc10Vw9mMX4GXXLW9kbkLz3kvPAA7lWo0ES2OG1tn4Tr4SNVAqdVB1KkMQEZG65Ij7ay-1fa4FMfskQERBpuO7OjmmDWqIvKcn3u_ftGCWnVUAB32QzdENR22FGwy89A6K83uzMiLKMB1Sc54FmIdb86VSgwPMdu2iZ7ISMyHbr7qEda6HxeTyGPW8LRJzURAD1nNto7aHuxAJdoSpogyLMrp-NpiYL8foNfIqMJ4nrEq48Rzj2oW5Djl9dTHNAQZ0_l4FP4-cxL5UZxTQ2ALVp40FX5HD9M5ieGT8uibCJJNjauO-ZxjVrIRQ0OCMUPfdHl71SuhBEAGrNE0LrEQ8BudduT4xW0ECoPqlxNy9nTwPhOLTOns2OqrvUQ9A7xmduqEoqOHe3kYOK4zxBuOLjN0dIGK95Oy45VdtFSS_OV9cyVSjebX4XqVVrJn1P7JGIuneAn37FgbCXIlBwzxS3N8KliER2qvrOLIafYQxowdcKtHAaNJSudQ4DLPaAR4jSLoHbkrddA-xYUY1QfBewqZNIjg3JXOjV-yDGkJ82LqlxTywWFRPslu-bezV4MawtJMdqqY5SqkJwzdylFZty8OCfn6ojIvrzRS2KDPiipG50aeYQxTwTaUtUP0nB1WqXTxOLxudBD0rqRRPYnp7q87A8em1KG-16Qea711JL11kh9RNZz1pNnuKtpybs-2ED7aaTk3zR7ZccO6yrWgqNTMApFssgt0jPZxUZP8oFmqTdJCF2AwqGOokVLuJ57F9_oH4ezSN2u0l7dtwnwogPwv9Ci2S4PjDZIffpqyY5YVylhDae8fX22Jfib_iAACdco8hBqBQCdcLEmPSK8FzT2Dp4tX-zRwgaasyE48SAvq25lwP2GCKvTQYvqXR1v8UZTAxZfzCZystrTv34mQshOdk8ZiFfFCXBGjUjohmSXBzHm6zQ22SnbNOJjQe6XpLk0QZtCJG543-6WN4QH2GycM0AgcbglV361T8HAPi5Yo4iKTYp1NWCyJDi4FQ_wGePMdo4WA_MCUU_zYaQGTkSLt0ESY2k5iFy-PgfW9OozY0e4WF2YGyBceRU2UdxrEVQ8-BFcz5yN3dfEMAejLkfF36v-l6wLhXcJZJKZdF85VoSXFvlX9k9X45_kahjZngsQ89-Znp4hms_JuLDtL8QvZdZDxwj0TiKd-f2JZmVD1qSrsUaLvGIgchCs7hN7tkQAR_7-Uwyy5eTe3cQh7WtMW_g2xlOksg2yyrUwLpjyed8Omwre6RLB1n7suRPXI8NzpZh1mOaeCd1uvfXiwIhdiT7kshS8Aod-qntWJZATUoVLFVfaQXLIKzsmpPqSH74TNHjyFb0Zj82TWzbJjFznkEDkp_OV4Lt7k0l0GBR0-50p-che6XyatjcODbY3E7obhsvPe9hncfO8UO2sxawX8XFuAvdiZZz7vdduiTYkevrG43vcDJc1Zilp5GcuwQIZ5IcmhVnZuJChnU_p5TIiTKgudgjR866xJe2KCBwx2ZRHVYwRw86tP9Beq4LlvWVyK5t0FdgOByYBMblLyiEf2rEo2HQ61yAXGC-sdXGT4OCj7Bm3hPXWCLWFT8GvWCOSggPAQIKh2AGA9MyH7AXbbZDPC02q8f7vVSfzfejp-TQcF_v38Uh88n_jwkK7U5HQz3dIOUZGXtA8iJJQSlN4rw82mMg6KshzDq4UkR5YvFtD6AmUxXquKWzx7S-ONaN9wRTnDKggyA83G_nmu7Yt3QOFJrYkgjqNKXTIJLTQPAD77t474Ooeq9yNMyjBJqh0bzimwN5GhlHDzxb-u9baM=","summary":[{"type":"summary_text","text":"**Investigating remote brain connection handling**"},{"type":"summary_text","text":"**Inspecting brain process run loop**"}]},{"id":"fc_07547ad47f060e5d016a58ab4a8e7c819abc902541eacdd4c7","type":"function_call","status":"completed","arguments":"{\"i\":\"Acknowledging hypothesis check\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Checking exact remote rc dispatch/serve_attach ownership now. Key correction so far: refresh brain boot uses cursor-only resume_session_cursors (no session subscription), so existing remote attach streams need explicit redispatch/re-serve; determining whether stream worker gets its own broker conn or dies with old brain.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_KiuKZ3vkKXVhgzXIOVtQnqfS","name":"irc"}]},"duration":13155.050100000575,"ttft":4151.537800000049,"contextSnapshot":{"promptTokens":64510,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KiuKZ3vkKXVhgzXIOVtQnqfS|fc_07547ad47f060e5d016a58ab4a8e7c819abc902541eacdd4c7","toolName":"irc","startedAt":"2026-07-16T09:58:37.743Z","intent":"Acknowledging hypothesis check"},"id":"9e692143","parentId":"e323ab6d","timestamp":"2026-07-16T09:58:37.743Z"}
{"type":"message","id":"af8c0418","parentId":"9e692143","timestamp":"2026-07-16T09:58:37.744Z","message":{"role":"toolResult","toolCallId":"call_KiuKZ3vkKXVhgzXIOVtQnqfS|fc_07547ad47f060e5d016a58ab4a8e7c819abc902541eacdd4c7","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"ReproHarness","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784195917744}}
{"type":"message","id":"05f85954","parentId":"af8c0418","timestamp":"2026-07-16T09:58:48.056Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_cNDcoYboRGAGiF0K2VfhdANm|fc_07547ad47f060e5d016a58ab577670819aa01d75e903add77b","name":"grep","arguments":{"i":"Tracing inbound dispatch lifecycle","pattern":"dispatch_inbound|dispatch.*stream|incoming|serve_attach|NetStreamOpened|net_streams\\(|initiated_locally|thread::spawn","path":"crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_9Amccy8UeQ54CrZbbxso25zl|fc_07547ad47f060e5d016a58ab5777a4819a84c99ac8d5dccebc","name":"read","arguments":{"i":"Reading brain process main loop","path":"crates/spt-daemon/src/brainproc.rs","selector":"270-490"}},{"type":"toolCall","id":"call_LGqq3fIbDPdOJ7G4FCypR79y|fc_07547ad47f060e5d016a58ab5777b4819abaaa59b1113fd2ee","name":"read","arguments":{"i":"Reading dispatcher implementation","path":"crates/spt-daemon/src/dispatch.rs","selector":"1-460"}},{"type":"toolCall","id":"call_VKnitMU6LarZ7sr37zMVXAND|fc_07547ad47f060e5d016a58ab5777c4819aac042ac7ac6a2054","name":"grep","arguments":{"i":"Finding reserve stream logic tests","pattern":"re-serve|reserve|redispatch|restart.*stream|stream.*restart|dispatch.*resume|survives.*brain","path":"crates/spt-daemon/tests;crates/spt/tests;docs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3865,"output":289,"cacheRead":60928,"cacheWrite":0,"totalTokens":65082,"cost":{"input":0.019325000000000002,"output":0.00867,"cacheRead":0.030463999999999998,"cacheWrite":0,"total":0.058459}},"stopReason":"toolUse","timestamp":1784195917760,"responseId":"resp_07547ad47f060e5d016a58ab4e32d0819aaebba1c5809e4b0b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_07547ad47f060e5d016a58ab577670819aa01d75e903add77b","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing inbound dispatch lifecycle\",\"pattern\":\"dispatch_inbound|dispatch.*stream|incoming|serve_attach|NetStreamOpened|net_streams\\\\(|initiated_locally|thread::spawn\",\"path\":\"crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_cNDcoYboRGAGiF0K2VfhdANm","name":"grep"},{"id":"fc_07547ad47f060e5d016a58ab5777a4819a84c99ac8d5dccebc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain process main loop\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"270-490\"}","call_id":"call_9Amccy8UeQ54CrZbbxso25zl","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab5777b4819abaaa59b1113fd2ee","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher implementation\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"1-460\"}","call_id":"call_LGqq3fIbDPdOJ7G4FCypR79y","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab5777c4819aac042ac7ac6a2054","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding reserve stream logic tests\",\"pattern\":\"re-serve|reserve|redispatch|restart.*stream|stream.*restart|dispatch.*resume|survives.*brain\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests;docs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_VKnitMU6LarZ7sr37zMVXAND","name":"grep"}]},"duration":10281.91300000064,"ttft":9690.085500000045,"contextSnapshot":{"promptTokens":64793,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cNDcoYboRGAGiF0K2VfhdANm|fc_07547ad47f060e5d016a58ab577670819aa01d75e903add77b","toolName":"grep","startedAt":"2026-07-16T09:58:48.056Z","args":{"path":"crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs"},"intent":"Tracing inbound dispatch lifecycle"},"id":"d88f55f0","parentId":"05f85954","timestamp":"2026-07-16T09:58:48.056Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9Amccy8UeQ54CrZbbxso25zl|fc_07547ad47f060e5d016a58ab5777a4819a84c99ac8d5dccebc","toolName":"read","startedAt":"2026-07-16T09:58:48.056Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain process main loop"},"id":"ff969e82","parentId":"d88f55f0","timestamp":"2026-07-16T09:58:48.056Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_LGqq3fIbDPdOJ7G4FCypR79y|fc_07547ad47f060e5d016a58ab5777b4819abaaa59b1113fd2ee","toolName":"read","startedAt":"2026-07-16T09:58:48.056Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher implementation"},"id":"8d61c5dc","parentId":"ff969e82","timestamp":"2026-07-16T09:58:48.056Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VKnitMU6LarZ7sr37zMVXAND|fc_07547ad47f060e5d016a58ab5777c4819aac042ac7ac6a2054","toolName":"grep","startedAt":"2026-07-16T09:58:48.056Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests;docs"},"intent":"Finding reserve stream logic tests"},"id":"84a75de4","parentId":"8d61c5dc","timestamp":"2026-07-16T09:58:48.056Z"}
{"type":"message","id":"f6e47324","parentId":"84a75de4","timestamp":"2026-07-16T09:58:48.068Z","message":{"role":"toolResult","toolCallId":"call_LGqq3fIbDPdOJ7G4FCypR79y|fc_07547ad47f060e5d016a58ab5777b4819abaaa59b1113fd2ee","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;\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/// 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    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).\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/// 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                Some(endpoint) => {\n                    let root = spt_store::xfer::transfers_dir();\n                    let progress = paths.scratch.join(\"xfer-progress\");\n                    match serve_xfer(\n                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\n                    ) {\n                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n                    }\n                }\n                None => {\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n                }\n            }\n        }\n        StreamFamily::Rest => {\n            // The wire names whose whitelist gates (the xfer posture); the\n            // origin stays handshake-proven. The serve applies the event\n            // through the LOCAL transition host — echo gate, wake effects,\n            // and shell cascade run on this node, where the instance lives.\n            // [impl->REQ-INST-6]\n            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ShellLink => {\n            // The wire names the owner whose whitelist gates AND whose shell\n            // tree resolves (owner exclusivity = resolution scope); the\n            // action runs against the LOCAL owlery — perch, manifest, spool,\n            // and binary all live here, the shell's node.\n            // [impl->REQ-SHELL-2]\n            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\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\n[Showing lines 1-463 of 759. Use :464 to continue]"}],"details":{"truncation":{"content":"//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\r\n//! the hand-wired serve loops of D5–D8 into production behavior.\r\n//!\r\n//! Every E2E test so far drove the serve half manually (a probe brain calling\r\n//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\r\n//! a known stream). In production nothing did — a peer could dial and write\r\n//! and the bytes sat in the broker's stream log unread. This module is the\r\n//! missing consumer: a daemon-hosted loop that watches the broker's stream\r\n//! table for **peer-initiated** streams, classifies each by its first NDJSON\r\n//! record, and hands it to the right serve loop on its own worker thread.\r\n//!\r\n//! ## Direction (the gotcha-#9 fact, now first-class)\r\n//!\r\n//! The stream table holds locally-opened outbound streams too. The dispatcher\r\n//! claims only rows with `initiated_locally == false` — serving our own\r\n//! requester stream would deadlock the requester against ourselves. The flag\r\n//! is broker-recorded at registration (`open_stream` vs the accept loop),\r\n//! never inferred.\r\n//!\r\n//! ## Classification (first-record demux)\r\n//!\r\n//! Streams are single-purpose; the opener's first record names the protocol.\r\n//! The kind-tagged families collide on variant names (`request` is both\r\n//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\r\n//! demux keys on the first line's **shape**, not its kind alone:\r\n//!\r\n//! | first line | family |\r\n//! |---|---|\r\n//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\r\n//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\r\n//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\r\n//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\r\n//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\r\n//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\r\n//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\r\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\r\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\r\n//! | anything else | unknown — dropped, loud |\r\n//!\r\n//! A family whose production wiring lands in a later slice is still\r\n//! classified here (the table is complete) but routed to a typed\r\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\r\n//!\r\n//! ## Worker model\r\n//!\r\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\r\n//! connection (the broker is multi-client by design; the probe-brain pattern\r\n//! every E2E test uses). The peek and the serve use **separate** connections:\r\n//! the peek brain subscribes from 0 to read the first line and is dropped;\r\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\r\n//! stream's event feed alone (no double-subscription interleave). Origin is\r\n//! the table's `remote_id_hex` — handshake-proven, never payload\r\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\r\n// [impl->REQ-NET-1]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n\r\nuse std::collections::HashSet;\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse spt_net::net::notif::NotifDecoder;\r\nuse spt_net::net::replicate::{\r\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\r\n};\r\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\r\nuse spt_store::contextstore::ContextStore;\r\nuse spt_store::notif::NotifStore;\r\nuse spt_store::perch;\r\nuse spt_store::roster::RosterStore;\r\n\r\nuse crate::attach::serve_attach;\r\nuse crate::brain::{Brain, BrokerEvent};\r\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\r\nuse crate::propagate::serve_update;\r\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\r\nuse crate::relcache::ReleaseCache;\r\nuse crate::sync::{serve_sync, SyncPolicy};\r\nuse crate::wan::receive_wan;\r\nuse crate::xfer::serve_xfer;\r\n\r\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\r\n/// workers block on their stream's event feed).\r\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\r\n\r\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum StreamFamily {\r\n    /// A context sync pull (`SyncRecord::Request`).\r\n    Sync,\r\n    /// An update pull (`UpdRecord::Query`).\r\n    Update,\r\n    /// A notif replication feed (`NotifRecord::Row`).\r\n    Notif,\r\n    /// A WAN message feed (untagged `WanMessage` lines).\r\n    WanMsg,\r\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\r\n    Registry,\r\n    /// A remote-drive attach (`AttachRecord::Request`).\r\n    Attach,\r\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\r\n    Xfer,\r\n    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\r\n    /// node, M5-D5b).\r\n    Rest,\r\n    /// A cross-node shell-link op (`ShellLinkRecord::Request` — relink/cmd\r\n    /// against an owner's shell on this node, M5-D8c).\r\n    ShellLink,\r\n    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\r\n    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\r\n    ServeProbe,\r\n    /// Unclassifiable — dropped.\r\n    Unknown,\r\n}\r\n\r\n/// Classify one decoded first line by shape (module-docs demux table).\r\npub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {\r\n    match line.get(\"kind\").and_then(|k| k.as_str()) {\r\n        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\r\n        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\r\n        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\r\n            StreamFamily::Rest\r\n        }\r\n        Some(\"request\") if line.get(\"owner\").is_some() && line.get(\"shell_ref\").is_some() => {\r\n            StreamFamily::ShellLink\r\n        }\r\n        Some(\"request\") if line.get(\"serve_probe\").is_some() => StreamFamily::ServeProbe,\r\n        Some(\"query\") => StreamFamily::Update,\r\n        // The status-only convergence query rides the update family (M8\r\n        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\r\n        Some(\"status_query\") => StreamFamily::Update,\r\n        Some(\"row\") => StreamFamily::Notif,\r\n        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\r\n        Some(_) => StreamFamily::Unknown,\r\n        None => {\r\n            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\r\n                StreamFamily::WanMsg\r\n            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\r\n                StreamFamily::Registry\r\n            } else if line.get(\"subnet\").is_some()\r\n                && line.get(\"node\").is_some()\r\n                && line.get(\"label\").is_some()\r\n            {\r\n                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\r\n                // sends label records first (no instance rows), so the feed's\r\n                // first line is a NodeLabelUpdate. Same serve arm, same gate.\r\n                StreamFamily::Registry\r\n            } else {\r\n                StreamFamily::Unknown\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Why a dispatched stream's worker ended — loud + typed, never silent.\r\n#[derive(Debug)]\r\npub enum DispatchOutcome {\r\n    /// The family's serve loop ran to its own outcome (rendered for the log).\r\n    Served(String),\r\n    /// The first line never arrived / didn't parse — dropped.\r\n    Unknown,\r\n    /// The worker failed (IO against broker or store).\r\n    Failed(String),\r\n}\r\n\r\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\r\n/// worker (policies/stores re-load from the canonical paths per request —\r\n/// the access-gate stance, config edits take effect without a restart).\r\n#[derive(Debug, Clone)]\r\npub struct DispatchPaths {\r\n    /// The perch tree (WAN message funnel target).\r\n    pub owlery: PathBuf,\r\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\r\n    pub releases: PathBuf,\r\n    /// Transient bundle/artifact scratch.\r\n    pub scratch: PathBuf,\r\n}\r\n\r\nimpl DispatchPaths {\r\n    /// The production layout under [`perch::spt_home`].\r\n    pub fn production() -> DispatchPaths {\r\n        let home = perch::spt_home();\r\n        DispatchPaths {\r\n            owlery: perch::owlery_dir(),\r\n            releases: home.join(\"releases\"),\r\n            scratch: home.join(\"scratch\").join(\"net\"),\r\n        }\r\n    }\r\n}\r\n\r\n/// Watch the broker's stream table and dispatch every **peer-initiated**\r\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\r\n/// (the daemon spawns it on its own); workers detach (each ends with its\r\n/// stream).\r\npub fn run_dispatch_loop(\r\n    broker_name: &str,\r\n    paths: &DispatchPaths,\r\n    registry: Arc<RegistryHost>,\r\n    stop: &AtomicBool,\r\n    poll: Duration,\r\n) -> io::Result<()> {\r\n    let mut brain = connect(broker_name)?;\r\n    let mut claimed: HashSet<u64> = HashSet::new();\r\n\r\n    while !stop.load(Ordering::Relaxed) {\r\n        let reply = brain.net_streams()?;\r\n        for s in reply.streams {\r\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\r\n            // invisible until its first write anyway — gotcha #6 — but the\r\n            // next_seq guard also skips a row whose pump hasn't appended yet).\r\n            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\r\n                continue;\r\n            }\r\n            let name = broker_name.to_string();\r\n            let origin = s.remote_id_hex.clone();\r\n            let stream_id = s.stream_id;\r\n            let p = paths.clone();\r\n            let reg = Arc::clone(&registry);\r\n            thread::spawn(move || {\r\n                let outcome = worker(&name, stream_id, &origin, &p, &reg);\r\n                match outcome {\r\n                    DispatchOutcome::Served(_) => {}\r\n                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\r\n                }\r\n            });\r\n        }\r\n        thread::sleep(poll);\r\n    }\r\n    Ok(())\r\n}\r\n\r\n/// Connect a fresh brain IPC client (retrying through broker startup).\r\nfn connect(broker_name: &str) -> io::Result<Brain> {\r\n    let mut last = io::Error::other(\"broker never answered\");\r\n    for _ in 0..300 {\r\n        match Brain::cold_start(broker_name, now_ms()) {\r\n            Ok(b) => return Ok(b),\r\n            Err(e) => last = e,\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    Err(last)\r\n}\r\n\r\nfn now_ms() -> u64 {\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\r\n/// Peek the stream's first NDJSON line on a throwaway brain connection.\r\nfn peek_first_line(broker_name: &str, stream_id: u64) -> io::Result<Option<serde_json::Value>> {\r\n    let mut brain = connect(broker_name)?;\r\n    brain.net_stream_subscribe(stream_id, 0)?;\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\n\r\n/// One claimed stream's worker: peek → classify → run the family's serve\r\n/// loop on a fresh brain connection.\r\nfn worker(\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    origin: &str,\r\n    paths: &DispatchPaths,\r\n    registry: &RegistryHost,\r\n) -> DispatchOutcome {\r\n    let line = match peek_first_line(broker_name, stream_id) {\r\n        Ok(Some(v)) => v,\r\n        Ok(None) => return DispatchOutcome::Unknown,\r\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n    };\r\n    let family = classify_first_line(&line);\r\n\r\n    let mut brain = match connect(broker_name) {\r\n        Ok(b) => b,\r\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n    };\r\n\r\n    match family {\r\n        StreamFamily::Sync => {\r\n            let cs = match ContextStore::open_or_init() {\r\n                Ok(cs) => cs,\r\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n            };\r\n            let policy = SyncPolicy::load();\r\n            match serve_sync(\r\n                &mut brain,\r\n                stream_id,\r\n                origin,\r\n                0,\r\n                &cs,\r\n                &policy,\r\n                &paths.scratch,\r\n            ) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::Update => {\r\n            let cache = ReleaseCache::open(&paths.releases);\r\n            let roster = RosterStore::load();\r\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\r\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\r\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\r\n        StreamFamily::Attach => {\r\n            // The gate subject is the session's OWNING endpoint, resolved\r\n            // from the broker's table (the spawn's label) — never from wire\r\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\r\n            //\r\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\r\n            // cannot know our local session id, so it names the ENDPOINT. When the\r\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\r\n            // we actually host a live session for it (D5 session-presence gate;\r\n            // D6: a stale registry row that named a gone endpoint resolves to no\r\n            // session → refuse cleanly, never a false attach). The endpoint id is\r\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\r\n            // `endpoint`); the origin subject stays the handshake identity. The\r\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\r\n            // is unchanged.\r\n            // `endpoint` is the gate subject; `remote_session` carries the\r\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\r\n            // before serve_attach subscribes — never mid-serve, which would swallow\r\n            // interleaved attach records) and `None` on the local path.\r\n            let (endpoint, remote_session) = match line\r\n                .get(\"endpoint_id\")\r\n                .and_then(|v| v.as_str())\r\n                .filter(|e| !e.is_empty())\r\n            {\r\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\r\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\r\n                    None => (None, None),\r\n                },\r\n                None => {\r\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\r\n                    let endpoint = session_id.and_then(|sid| {\r\n                        brain.sessions().ok().and_then(|reply| {\r\n                            reply\r\n                                .sessions\r\n                                .iter()\r\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\r\n                                .map(|s| s.endpoint.clone())\r\n                        })\r\n                    });\r\n                    (endpoint, None)\r\n                }\r\n            };\r\n            match endpoint {\r\n                Some(endpoint) => match serve_attach(\r\n                    &mut brain,\r\n                    broker_name,\r\n                    stream_id,\r\n                    &endpoint,\r\n                    origin,\r\n                    0,\r\n                    remote_session,\r\n                ) {\r\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n                },\r\n                None => {\r\n                    // Refuse politely: finish our send side, claim stays.\r\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\r\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\r\n                }\r\n            }\r\n        }\r\n        StreamFamily::Xfer => {\r\n            // The wire names whose whitelist gates (the rule set); the origin\r\n            // it is checked against stays handshake-proven. Blank refuses.\r\n            let endpoint = line\r\n                .get(\"endpoint\")\r\n                .and_then(|v| v.as_str())\r\n                .filter(|e| !e.is_empty())\r\n                .map(str::to_string);\r\n            match endpoint {\r\n                Some(endpoint) => {\r\n                    let root = spt_store::xfer::transfers_dir();\r\n                    let progress = paths.scratch.join(\"xfer-progress\");\r\n                    match serve_xfer(\r\n                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\r\n                    ) {\r\n                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                        Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n                    }\r\n                }\r\n                None => {\r\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\r\n                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\r\n                }\r\n            }\r\n        }\r\n        StreamFamily::Rest => {\r\n            // The wire names whose whitelist gates (the xfer posture); the\r\n            // origin stays handshake-proven. The serve applies the event\r\n            // through the LOCAL transition host — echo gate, wake effects,\r\n            // and shell cascade run on this node, where the instance lives.\r\n            // [impl->REQ-INST-6]\r\n            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\r\n            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::ShellLink => {\r\n            // The wire names the owner whose whitelist gates AND whose shell\r\n            // tree resolves (owner exclusivity = resolution scope); the\r\n            // action runs against the LOCAL owlery — perch, manifest, spool,\r\n            // and binary all live here, the shell's node.\r\n            // [impl->REQ-SHELL-2]\r\n            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::ServeProbe => {\r\n            // No gate (serveprobe module docs): the answer is public\r\n            // serve-state for a named subnet, read fresh from this node's\r\n            // own stores — the QUIC handshake is the only subject.\r\n            // [impl->REQ-SUBNET-5]\r\n            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::Unknown => DispatchOutcome::Unknown,\r\n    }\r\n}\r\n\r\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\r\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\r\n/// origin trusted in that subnet), until the peer finishes.\r\n///\r\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\r","truncated":true,"truncatedBy":"lines","totalLines":759,"totalBytes":20831,"outputLines":463,"outputBytes":20831,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\r\n//! the hand-wired serve loops of D5–D8 into production behavior.\r\n//!\r\n//! Every E2E test so far drove the serve half manually (a probe brain calling\r\n//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\r\n//! a known stream). In production nothing did — a peer could dial and write\r\n//! and the bytes sat in the broker's stream log unread. This module is the\r\n//! missing consumer: a daemon-hosted loop that watches the broker's stream\r\n//! table for **peer-initiated** streams, classifies each by its first NDJSON\r\n//! record, and hands it to the right serve loop on its own worker thread.\r\n//!\r\n//! ## Direction (the gotcha-#9 fact, now first-class)\r\n//!\r\n//! The stream table holds locally-opened outbound streams too. The dispatcher\r\n//! claims only rows with `initiated_locally == false` — serving our own\r\n//! requester stream would deadlock the requester against ourselves. The flag\r\n//! is broker-recorded at registration (`open_stream` vs the accept loop),\r\n//! never inferred.\r\n//!\r\n//! ## Classification (first-record demux)\r\n//!\r\n//! Streams are single-purpose; the opener's first record names the protocol.\r\n//! The kind-tagged families collide on variant names (`request` is both\r\n//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\r\n//! demux keys on the first line's **shape**, not its kind alone:\r\n//!\r\n//! | first line | family |\r\n//! |---|---|\r\n//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\r\n//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\r\n//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\r\n//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\r\n//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\r\n//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\r\n//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\r\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\r\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\r\n//! | anything else | unknown — dropped, loud |\r\n//!\r\n//! A family whose production wiring lands in a later slice is still\r\n//! classified here (the table is complete) but routed to a typed\r\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\r\n//!\r\n//! ## Worker model\r\n//!\r\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\r\n//! connection (the broker is multi-client by design; the probe-brain pattern\r\n//! every E2E test uses). The peek and the serve use **separate** connections:\r\n//! the peek brain subscribes from 0 to read the first line and is dropped;\r\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\r\n//! stream's event feed alone (no double-subscription interleave). Origin is\r\n//! the table's `remote_id_hex` — handshake-proven, never payload\r\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\r\n// [impl->REQ-NET-1]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n\r\nuse std::collections::HashSet;\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse spt_net::net::notif::NotifDecoder;\r\nuse spt_net::net::replicate::{\r\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\r\n};\r\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\r\nuse spt_store::contextstore::ContextStore;\r\nuse spt_store::notif::NotifStore;\r\nuse spt_store::perch;\r\nuse spt_store::roster::RosterStore;\r\n\r\nuse crate::attach::serve_attach;\r\nuse crate::brain::{Brain, BrokerEvent};\r\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\r\nuse crate::propagate::serve_update;\r\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\r\nuse crate::relcache::ReleaseCache;\r\nuse crate::sync::{serve_sync, SyncPolicy};\r\nuse crate::wan::receive_wan;\r\nuse crate::xfer::serve_xfer;\r\n\r\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\r\n/// workers block on their stream's event feed).\r\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\r\n\r\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum StreamFamily {\r\n    /// A context sync pull (`SyncRecord::Request`).\r\n    Sync,\r\n    /// An update pull (`UpdRecord::Query`).\r\n    Update,\r\n    /// A notif replication feed (`NotifRecord::Row`).\r\n    Notif,\r\n    /// A WAN message feed (untagged `WanMessage` lines).\r\n    WanMsg,\r\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\r\n    Registry,\r\n    /// A remote-drive attach (`AttachRecord::Request`).\r\n    Attach,\r\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\r\n    Xfer,\r\n    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\r\n    /// node, M5-D5b).\r\n    Rest,\r\n    /// A cross-node shell-link op (`ShellLinkRecord::Request` — relink/cmd\r\n    /// against an owner's shell on this node, M5-D8c).\r\n    ShellLink,\r\n    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\r\n    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\r\n    ServeProbe,\r\n    /// Unclassifiable — dropped.\r\n    Unknown,\r\n}\r\n\r\n/// Classify one decoded first line by shape (module-docs demux table).\r\npub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {\r\n    match line.get(\"kind\").and_then(|k| k.as_str()) {\r\n        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\r\n        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\r\n        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\r\n            StreamFamily::Rest\r\n        }\r\n        Some(\"request\") if line.get(\"owner\").is_some() && line.get(\"shell_ref\").is_some() => {\r\n            StreamFamily::ShellLink\r\n        }\r\n        Some(\"request\") if line.get(\"serve_probe\").is_some() => StreamFamily::ServeProbe,\r\n        Some(\"query\") => StreamFamily::Update,\r\n        // The status-only convergence query rides the update family (M8\r\n        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\r\n        Some(\"status_query\") => StreamFamily::Update,\r\n        Some(\"row\") => StreamFamily::Notif,\r\n        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\r\n        Some(_) => StreamFamily::Unknown,\r\n        None => {\r\n            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\r\n                StreamFamily::WanMsg\r\n            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\r\n                StreamFamily::Registry\r\n            } else if line.get(\"subnet\").is_some()\r\n                && line.get(\"node\").is_some()\r\n                && line.get(\"label\").is_some()\r\n            {\r\n                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\r\n                // sends label records first (no instance rows), so the feed's\r\n                // first line is a NodeLabelUpdate. Same serve arm, same gate.\r\n                StreamFamily::Registry\r\n            } else {\r\n                StreamFamily::Unknown\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Why a dispatched stream's worker ended — loud + typed, never silent.\r\n#[derive(Debug)]\r\npub enum DispatchOutcome {\r\n    /// The family's serve loop ran to its own outcome (rendered for the log).\r\n    Served(String),\r\n    /// The first line never arrived / didn't parse — dropped.\r\n    Unknown,\r\n    /// The worker failed (IO against broker or store).\r\n    Failed(String),\r\n}\r\n\r\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\r\n/// worker (policies/stores re-load from the canonical paths per request —\r\n/// the access-gate stance, config edits take effect without a restart).\r\n#[derive(Debug, Clone)]\r\npub struct DispatchPaths {\r\n    /// The perch tree (WAN message funnel target).\r\n    pub owlery: PathBuf,\r\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\r\n    pub releases: PathBuf,\r\n    /// Transient bundle/artifact scratch.\r\n    pub scratch: PathBuf,\r\n}\r\n\r\nimpl DispatchPaths {\r\n    /// The production layout under [`perch::spt_home`].\r\n    pub fn production() -> DispatchPaths {\r\n        let home = perch::spt_home();\r\n        DispatchPaths {\r\n            owlery: perch::owlery_dir(),\r\n            releases: home.join(\"releases\"),\r\n            scratch: home.join(\"scratch\").join(\"net\"),\r\n        }\r\n    }\r\n}\r\n\r\n/// Watch the broker's stream table and dispatch every **peer-initiated**\r\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\r\n/// (the daemon spawns it on its own); workers detach (each ends with its\r\n/// stream).\r\npub fn run_dispatch_loop(\r\n    broker_name: &str,\r\n    paths: &DispatchPaths,\r\n    registry: Arc<RegistryHost>,\r\n    stop: &AtomicBool,\r\n    poll: Duration,\r\n) -> io::Result<()> {\r\n    let mut brain = connect(broker_name)?;\r\n    let mut claimed: HashSet<u64> = HashSet::new();\r\n\r\n    while !stop.load(Ordering::Relaxed) {\r\n        let reply = brain.net_streams()?;\r\n        for s in reply.streams {\r\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\r\n            // invisible until its first write anyway — gotcha #6 — but the\r\n            // next_seq guard also skips a row whose pump hasn't appended yet).\r\n            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\r\n                continue;\r\n            }\r\n            let name = broker_name.to_string();\r\n            let origin = s.remote_id_hex.clone();\r\n            let stream_id = s.stream_id;\r\n            let p = paths.clone();\r\n            let reg = Arc::clone(&registry);\r\n            thread::spawn(move || {\r\n                let outcome = worker(&name, stream_id, &origin, &p, &reg);\r\n                match outcome {\r\n                    DispatchOutcome::Served(_) => {}\r\n                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\r\n                }\r\n            });\r\n        }\r\n        thread::sleep(poll);\r\n    }\r\n    Ok(())\r\n}\r\n\r\n/// Connect a fresh brain IPC client (retrying through broker startup).\r\nfn connect(broker_name: &str) -> io::Result<Brain> {\r\n    let mut last = io::Error::other(\"broker never answered\");\r\n    for _ in 0..300 {\r\n        match Brain::cold_start(broker_name, now_ms()) {\r\n            Ok(b) => return Ok(b),\r\n            Err(e) => last = e,\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    Err(last)\r\n}\r\n\r\nfn now_ms() -> u64 {\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\r\n/// Peek the stream's first NDJSON line on a throwaway brain connection.\r\nfn peek_first_line(broker_name: &str, stream_id: u64) -> io::Result<Option<serde_json::Value>> {\r\n    let mut brain = connect(broker_name)?;\r\n    brain.net_stream_subscribe(stream_id, 0)?;\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\n\r\n/// One claimed stream's worker: peek → classify → run the family's serve\r\n/// loop on a fresh brain connection.\r\nfn worker(\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    origin: &str,\r\n    paths: &DispatchPaths,\r\n    registry: &RegistryHost,\r\n) -> DispatchOutcome {\r\n    let line = match peek_first_line(broker_name, stream_id) {\r\n        Ok(Some(v)) => v,\r\n        Ok(None) => return DispatchOutcome::Unknown,\r\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n    };\r\n    let family = classify_first_line(&line);\r\n\r\n    let mut brain = match connect(broker_name) {\r\n        Ok(b) => b,\r\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n    };\r\n\r\n    match family {\r\n        StreamFamily::Sync => {\r\n            let cs = match ContextStore::open_or_init() {\r\n                Ok(cs) => cs,\r\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n            };\r\n            let policy = SyncPolicy::load();\r\n            match serve_sync(\r\n                &mut brain,\r\n                stream_id,\r\n                origin,\r\n                0,\r\n                &cs,\r\n                &policy,\r\n                &paths.scratch,\r\n            ) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::Update => {\r\n            let cache = ReleaseCache::open(&paths.releases);\r\n            let roster = RosterStore::load();\r\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\r\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\r\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\r\n        StreamFamily::Attach => {\r\n            // The gate subject is the session's OWNING endpoint, resolved\r\n            // from the broker's table (the spawn's label) — never from wire\r\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\r\n            //\r\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\r\n            // cannot know our local session id, so it names the ENDPOINT. When the\r\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\r\n            // we actually host a live session for it (D5 session-presence gate;\r\n            // D6: a stale registry row that named a gone endpoint resolves to no\r\n            // session → refuse cleanly, never a false attach). The endpoint id is\r\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\r\n            // `endpoint`); the origin subject stays the handshake identity. The\r\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\r\n            // is unchanged.\r\n            // `endpoint` is the gate subject; `remote_session` carries the\r\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\r\n            // before serve_attach subscribes — never mid-serve, which would swallow\r\n            // interleaved attach records) and `None` on the local path.\r\n            let (endpoint, remote_session) = match line\r\n                .get(\"endpoint_id\")\r\n                .and_then(|v| v.as_str())\r\n                .filter(|e| !e.is_empty())\r\n            {\r\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\r\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\r\n                    None => (None, None),\r\n                },\r\n                None => {\r\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\r\n                    let endpoint = session_id.and_then(|sid| {\r\n                        brain.sessions().ok().and_then(|reply| {\r\n                            reply\r\n                                .sessions\r\n                                .iter()\r\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\r\n                                .map(|s| s.endpoint.clone())\r\n                        })\r\n                    });\r\n                    (endpoint, None)\r\n                }\r\n            };\r\n            match endpoint {\r\n                Some(endpoint) => match serve_attach(\r\n                    &mut brain,\r\n                    broker_name,\r\n                    stream_id,\r\n                    &endpoint,\r\n                    origin,\r\n                    0,\r\n                    remote_session,\r\n                ) {\r\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n                },\r\n                None => {\r\n                    // Refuse politely: finish our send side, claim stays.\r\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\r\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\r\n                }\r\n            }\r\n        }\r\n        StreamFamily::Xfer => {\r\n            // The wire names whose whitelist gates (the rule set); the origin\r\n            // it is checked against stays handshake-proven. Blank refuses.\r\n            let endpoint = line\r\n                .get(\"endpoint\")\r\n                .and_then(|v| v.as_str())\r\n                .filter(|e| !e.is_empty())\r\n                .map(str::to_string);\r\n            match endpoint {\r\n                Some(endpoint) => {\r\n                    let root = spt_store::xfer::transfers_dir();\r\n                    let progress = paths.scratch.join(\"xfer-progress\");\r\n                    match serve_xfer(\r\n                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\r\n                    ) {\r\n                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                        Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n                    }\r\n                }\r\n                None => {\r\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\r\n                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\r\n                }\r\n            }\r\n        }\r\n        StreamFamily::Rest => {\r\n            // The wire names whose whitelist gates (the xfer posture); the\r\n            // origin stays handshake-proven. The serve applies the event\r\n            // through the LOCAL transition host — echo gate, wake effects,\r\n            // and shell cascade run on this node, where the instance lives.\r\n            // [impl->REQ-INST-6]\r\n            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\r\n            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::ShellLink => {\r\n            // The wire names the owner whose whitelist gates AND whose shell\r\n            // tree resolves (owner exclusivity = resolution scope); the\r\n            // action runs against the LOCAL owlery — perch, manifest, spool,\r\n            // and binary all live here, the shell's node.\r\n            // [impl->REQ-SHELL-2]\r\n            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::ServeProbe => {\r\n            // No gate (serveprobe module docs): the answer is public\r\n            // serve-state for a named subnet, read fresh from this node's\r\n            // own stores — the QUIC handshake is the only subject.\r\n            // [impl->REQ-SUBNET-5]\r\n            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {\r\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\r\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\r\n            }\r\n        }\r\n        StreamFamily::Unknown => DispatchOutcome::Unknown,\r\n    }\r\n}\r\n\r\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\r\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\r\n/// origin trusted in that subnet), until the peer finishes.\r\n///\r\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":759,"totalBytes":20831,"outputLines":463,"outputBytes":20831,"shownRange":{"start":1,"end":463},"nextOffset":464}}},"isError":false,"timestamp":1784195928068}}
{"type":"message","id":"77744176","parentId":"f6e47324","timestamp":"2026-07-16T09:58:48.082Z","message":{"role":"toolResult","toolCallId":"call_9Amccy8UeQ54CrZbbxso25zl|fc_07547ad47f060e5d016a58ab5777a4819a84c99ac8d5dccebc","toolName":"read","content":[{"type":"text","text":"pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n…\n    loop {\n…\n        let status = match brain.net_status() {\n…\n            Err(e) => {\n…\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/// Connect to the broker, retrying briefly while it finishes binding.\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n    let mut last: Option<io::Error> = None;\n    for _ in 0..tries {\n        match Brain::cold_start(name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => {\n                last = Some(e);\n                thread::sleep(Duration::from_millis(10));\n            }\n        }\n    }\n    Err(last.unwrap_or_else(|| io::Error::other(\"brain could not connect to broker\")))\n}\n\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\n/// flips across a swap; the D7-4 field run asserts it matches the released\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn write_ready(generation: u64) {\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\n    if let Some(hash) = current_exe_hash() {\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n    }\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n}\n\n/// Lowercase-hex SHA-256 of the running process's own executable file\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n/// read + hash per brain start — negligible, and it pays rent far beyond the\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\nfn current_exe_hash() -> Option<String> {\n    let exe = std::env::current_exe().ok()?;\n    let bytes = std::fs::read(exe).ok()?;\n    Some(crate::release::sha256_hex(&bytes))\n}\n\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_generation(body: &str) -> Option<u64> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"generation\")?\n        .as_u64()\n}\n\n/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\n/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\n/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\n/// this to prove a respawn ran the swapped binary's bytes.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"exe_hash\")?\n        .as_str()\n        .map(str::to_string)\n}\n\n/// Read the current `brain.ready` generation stamp from `path`, `None` if the\n/// file is absent or its body has no parseable generation (fail-safe to\n/// not-ready). The supervisor's \"is generation N ready?\" gate (D6-2) reads\n/// [`brain_ready_path`]; the path is a parameter here so the unit harness points\n/// it at a temp file. Restoration D6-1b.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n}\n\n/// The supervisor's next backoff after a brain death: a healthy run (≥\n/// [`SUPERVISE_HEALTHY_RUN`]) resets to the floor `base`; a fast death doubles,\n/// capped at [`SUPERVISE_BACKOFF_CAP`]. Pure for the unit table (parallels\n/// `pump::next_backoff`).\nfn next_backoff(prev: Duration, ran_for: Duration, base: Duration) -> Duration {\n    if ran_for >= SUPERVISE_HEALTHY_RUN {\n        base\n    } else {\n        prev.saturating_mul(2).min(SUPERVISE_BACKOFF_CAP)\n    }\n}\n\n/// The supervisor's rollback context (ADR-0018 Q7, restoration D6-2): the\n/// durable two-phase applied record, the generation-stamped readiness probe,\n/// and the promote/rollback effects — all **injected** (exactly as `spawn_child`\n/// is) so the unit harness drives the readiness-gated state machine\n/// deterministically with a scripted ready/no-ready + a capturing notif sink,\n/// while production binds the real `spt_home` + relcache + notif sender\n/// ([`ProductionTrialEnv`]). Every method is **best-effort / fail-safe**: a\n/// missing record reads as \"no pending trial\", an absent ready file as\n/// \"not ready\" — the broker's supervisor thread never panics (a panic there\n/// orphans the brain).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub trait TrialEnv {\n    /// The current two-phase applied record, or `None` for no pending trial\n    /// (absent/corrupt degrades to `None`).\n    fn applied_state(&self) -> Option<AppliedRecord>;\n    /// The generation stamped in `brain.ready`, or `None` if absent/garbled\n    /// (the readiness gate reads `None` as not-ready — fail-safe).\n    fn ready_generation(&self) -> Option<u64>;\n    /// Delete `brain.ready` immediately before a trial spawn (**A9** — close the\n    /// cross-lifetime staleness hole: the in-memory generation resets to 0 on a\n    /// broker restart, so a stale gen-0 file would otherwise match a fresh gen-0\n    /// trial and falsely promote a brain that never booted).\n    fn clear_ready(&self);\n    /// Promote the candidate: it reached ready. Persist `Applied{version}` plus\n    /// the legacy `applied.json`/`last-outcome=applied` convergence records\n    /// (relocated here from `apply`, where they were the optimistic enlyzeam\n    /// write — promotion is the *real* moment the new code is accepted).\n    fn record_promoted(&self, version: u64);\n    /// Correct the record: the candidate failed readiness. Persist\n    /// `RolledBack{…}` and fire the loud, resurfacing rollback notif.\n    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);\n    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is\n    /// actually running), or `None` if absent/garbled — degrades the promotion\n    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\n    fn ready_exe_hash(&self) -> Option<String>;\n    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\n    /// the candidate SHOULD be running), or `None` if no staged metadata /\n    /// version mismatch / platform absent — degrades the gate to readiness-only.\n    fn staged_artifact_hash(&self, version: u64) -> Option<String>;\n    /// Whether the OLD brain generation's broker subscriber connection is DRAINED —\n    /// its local controller conn CLOSED or stall-evicted, no blocked writes still\n    /// pending on it (REQ-UPDATE-PROMOTE-DRAINED). Reads BROKER TRUTH in-process (no\n    /// brain round-trip): the supervisor promotes a ready candidate only once this is\n    /// `true`, so `brain.ready` arriving inside the W2 `BRAIN_WRITE_DEADLINE` window\n    /// can no longer promote onto a still-frozen control plane (the 22:47 false-\n    /// promote). Fail-safe toward DRAINED (`true`) where there is nothing to read — a\n    /// net-less broker, no hosted sessions, or [`NoTrialEnv`] — so the gate never\n    /// strands an apply where no old-gen control plane exists.\n    fn old_gen_drained(&self) -> bool;\n}\n\n/// The no-trial environment: every probe degrades to \"nothing pending / not\n/// ready\" and every effect is a no-op. Used where there is no readiness gate to\n/// drive — the supervisor then behaves exactly as the pre-D6 respawn loop.\npub struct NoTrialEnv;\n\nimpl TrialEnv for NoTrialEnv {\n    fn applied_state(&self) -> Option<AppliedRecord> {\n        None\n    }\n…\n}\n\n[Showing lines 269-493 of 2116. Use :494 to continue]"}],"details":{"truncation":{"content":"            }\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\n\r\n/// Connect to the broker, retrying briefly while it finishes binding.\r\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\r\n    let mut last: Option<io::Error> = None;\r\n    for _ in 0..tries {\r\n        match Brain::cold_start(name, now_ms()) {\r\n            Ok(b) => return Ok(b),\r\n            Err(e) => {\r\n                last = Some(e);\r\n                thread::sleep(Duration::from_millis(10));\r\n            }\r\n        }\r\n    }\r\n    Err(last.unwrap_or_else(|| io::Error::other(\"brain could not connect to broker\")))\r\n}\r\n\r\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\r\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\r\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\r\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\r\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\r\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\r\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\r\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\r\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\r\n/// flips across a swap; the D7-4 field run asserts it matches the released\r\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\r\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\r\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\r\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn write_ready(generation: u64) {\r\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\r\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\r\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\r\n    if let Some(hash) = current_exe_hash() {\r\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\r\n    }\r\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\r\n}\r\n\r\n/// Lowercase-hex SHA-256 of the running process's own executable file\r\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\r\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\r\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\r\n/// read + hash per brain start — negligible, and it pays rent far beyond the\r\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\r\nfn current_exe_hash() -> Option<String> {\r\n    let exe = std::env::current_exe().ok()?;\r\n    let bytes = std::fs::read(exe).ok()?;\r\n    Some(crate::release::sha256_hex(&bytes))\r\n}\r\n\r\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\r\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\r\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\r\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn parse_ready_generation(body: &str) -> Option<u64> {\r\n    serde_json::from_str::<serde_json::Value>(body)\r\n        .ok()?\r\n        .get(\"generation\")?\r\n        .as_u64()\r\n}\r\n\r\n/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\r\n/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\r\n/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\r\n/// this to prove a respawn ran the swapped binary's bytes.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn parse_ready_exe_hash(body: &str) -> Option<String> {\r\n    serde_json::from_str::<serde_json::Value>(body)\r\n        .ok()?\r\n        .get(\"exe_hash\")?\r\n        .as_str()\r\n        .map(str::to_string)\r\n}\r\n\r\n/// Read the current `brain.ready` generation stamp from `path`, `None` if the\r\n/// file is absent or its body has no parseable generation (fail-safe to\r\n/// not-ready). The supervisor's \"is generation N ready?\" gate (D6-2) reads\r\n/// [`brain_ready_path`]; the path is a parameter here so the unit harness points\r\n/// it at a temp file. Restoration D6-1b.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\r\n    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\r\n}\r\n\r\n/// The supervisor's next backoff after a brain death: a healthy run (≥\r\n/// [`SUPERVISE_HEALTHY_RUN`]) resets to the floor `base`; a fast death doubles,\r\n/// capped at [`SUPERVISE_BACKOFF_CAP`]. Pure for the unit table (parallels\r\n/// `pump::next_backoff`).\r\nfn next_backoff(prev: Duration, ran_for: Duration, base: Duration) -> Duration {\r\n    if ran_for >= SUPERVISE_HEALTHY_RUN {\r\n        base\r\n    } else {\r\n        prev.saturating_mul(2).min(SUPERVISE_BACKOFF_CAP)\r\n    }\r\n}\r\n\r\n/// The supervisor's rollback context (ADR-0018 Q7, restoration D6-2): the\r\n/// durable two-phase applied record, the generation-stamped readiness probe,\r\n/// and the promote/rollback effects — all **injected** (exactly as `spawn_child`\r\n/// is) so the unit harness drives the readiness-gated state machine\r\n/// deterministically with a scripted ready/no-ready + a capturing notif sink,\r\n/// while production binds the real `spt_home` + relcache + notif sender\r\n/// ([`ProductionTrialEnv`]). Every method is **best-effort / fail-safe**: a\r\n/// missing record reads as \"no pending trial\", an absent ready file as\r\n/// \"not ready\" — the broker's supervisor thread never panics (a panic there\r\n/// orphans the brain).\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub trait TrialEnv {\r\n    /// The current two-phase applied record, or `None` for no pending trial\r\n    /// (absent/corrupt degrades to `None`).\r\n    fn applied_state(&self) -> Option<AppliedRecord>;\r\n    /// The generation stamped in `brain.ready`, or `None` if absent/garbled\r\n    /// (the readiness gate reads `None` as not-ready — fail-safe).\r\n    fn ready_generation(&self) -> Option<u64>;\r\n    /// Delete `brain.ready` immediately before a trial spawn (**A9** — close the\r\n    /// cross-lifetime staleness hole: the in-memory generation resets to 0 on a\r\n    /// broker restart, so a stale gen-0 file would otherwise match a fresh gen-0\r\n    /// trial and falsely promote a brain that never booted).\r\n    fn clear_ready(&self);\r\n    /// Promote the candidate: it reached ready. Persist `Applied{version}` plus\r\n    /// the legacy `applied.json`/`last-outcome=applied` convergence records\r\n    /// (relocated here from `apply`, where they were the optimistic enlyzeam\r\n    /// write — promotion is the *real* moment the new code is accepted).\r\n    fn record_promoted(&self, version: u64);\r\n    /// Correct the record: the candidate failed readiness. Persist\r\n    /// `RolledBack{…}` and fire the loud, resurfacing rollback notif.\r\n    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);\r\n    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is\r\n    /// actually running), or `None` if absent/garbled — degrades the promotion\r\n    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\r\n    fn ready_exe_hash(&self) -> Option<String>;\r\n    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\r\n    /// the candidate SHOULD be running), or `None` if no staged metadata /\r\n    /// version mismatch / platform absent — degrades the gate to readiness-only.\r\n    fn staged_artifact_hash(&self, version: u64) -> Option<String>;\r\n    /// Whether the OLD brain generation's broker subscriber connection is DRAINED —\r\n    /// its local controller conn CLOSED or stall-evicted, no blocked writes still\r\n    /// pending on it (REQ-UPDATE-PROMOTE-DRAINED). Reads BROKER TRUTH in-process (no\r\n    /// brain round-trip): the supervisor promotes a ready candidate only once this is\r\n    /// `true`, so `brain.ready` arriving inside the W2 `BRAIN_WRITE_DEADLINE` window\r\n    /// can no longer promote onto a still-frozen control plane (the 22:47 false-\r\n    /// promote). Fail-safe toward DRAINED (`true`) where there is nothing to read — a\r\n    /// net-less broker, no hosted sessions, or [`NoTrialEnv`] — so the gate never\r\n    /// strands an apply where no old-gen control plane exists.\r\n    fn old_gen_drained(&self) -> bool;\r\n}\r\n\r\n/// The no-trial environment: every probe degrades to \"nothing pending / not\r\n/// ready\" and every effect is a no-op. Used where there is no readiness gate to\r\n/// drive — the supervisor then behaves exactly as the pre-D6 respawn loop.\r\npub struct NoTrialEnv;\r\n\r\nimpl TrialEnv for NoTrialEnv {\r\n    fn applied_state(&self) -> Option<AppliedRecord> {\r\n        None\r\n    }\r","truncated":true,"truncatedBy":"lines","totalLines":1848,"totalBytes":12585,"outputLines":225,"outputBytes":12585,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n…\n    loop {\n…\n        let status = match brain.net_status() {\n…\n            Err(e) => {\n…\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\n\r\n/// Connect to the broker, retrying briefly while it finishes binding.\r\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\r\n    let mut last: Option<io::Error> = None;\r\n    for _ in 0..tries {\r\n        match Brain::cold_start(name, now_ms()) {\r\n            Ok(b) => return Ok(b),\r\n            Err(e) => {\r\n                last = Some(e);\r\n                thread::sleep(Duration::from_millis(10));\r\n            }\r\n        }\r\n    }\r\n    Err(last.unwrap_or_else(|| io::Error::other(\"brain could not connect to broker\")))\r\n}\r\n\r\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\r\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\r\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\r\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\r\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\r\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\r\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\r\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\r\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\r\n/// flips across a swap; the D7-4 field run asserts it matches the released\r\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\r\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\r\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\r\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn write_ready(generation: u64) {\r\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\r\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\r\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\r\n    if let Some(hash) = current_exe_hash() {\r\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\r\n    }\r\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\r\n}\r\n\r\n/// Lowercase-hex SHA-256 of the running process's own executable file\r\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\r\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\r\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\r\n/// read + hash per brain start — negligible, and it pays rent far beyond the\r\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\r\nfn current_exe_hash() -> Option<String> {\r\n    let exe = std::env::current_exe().ok()?;\r\n    let bytes = std::fs::read(exe).ok()?;\r\n    Some(crate::release::sha256_hex(&bytes))\r\n}\r\n\r\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\r\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\r\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\r\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn parse_ready_generation(body: &str) -> Option<u64> {\r\n    serde_json::from_str::<serde_json::Value>(body)\r\n        .ok()?\r\n        .get(\"generation\")?\r\n        .as_u64()\r\n}\r\n\r\n/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\r\n/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\r\n/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\r\n/// this to prove a respawn ran the swapped binary's bytes.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn parse_ready_exe_hash(body: &str) -> Option<String> {\r\n    serde_json::from_str::<serde_json::Value>(body)\r\n        .ok()?\r\n        .get(\"exe_hash\")?\r\n        .as_str()\r\n        .map(str::to_string)\r\n}\r\n\r\n/// Read the current `brain.ready` generation stamp from `path`, `None` if the\r\n/// file is absent or its body has no parseable generation (fail-safe to\r\n/// not-ready). The supervisor's \"is generation N ready?\" gate (D6-2) reads\r\n/// [`brain_ready_path`]; the path is a parameter here so the unit harness points\r\n/// it at a temp file. Restoration D6-1b.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\r\n    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\r\n}\r\n\r\n/// The supervisor's next backoff after a brain death: a healthy run (≥\r\n/// [`SUPERVISE_HEALTHY_RUN`]) resets to the floor `base`; a fast death doubles,\r\n/// capped at [`SUPERVISE_BACKOFF_CAP`]. Pure for the unit table (parallels\r\n/// `pump::next_backoff`).\r\nfn next_backoff(prev: Duration, ran_for: Duration, base: Duration) -> Duration {\r\n    if ran_for >= SUPERVISE_HEALTHY_RUN {\r\n        base\r\n    } else {\r\n        prev.saturating_mul(2).min(SUPERVISE_BACKOFF_CAP)\r\n    }\r\n}\r\n\r\n/// The supervisor's rollback context (ADR-0018 Q7, restoration D6-2): the\r\n/// durable two-phase applied record, the generation-stamped readiness probe,\r\n/// and the promote/rollback effects — all **injected** (exactly as `spawn_child`\r\n/// is) so the unit harness drives the readiness-gated state machine\r\n/// deterministically with a scripted ready/no-ready + a capturing notif sink,\r\n/// while production binds the real `spt_home` + relcache + notif sender\r\n/// ([`ProductionTrialEnv`]). Every method is **best-effort / fail-safe**: a\r\n/// missing record reads as \"no pending trial\", an absent ready file as\r\n/// \"not ready\" — the broker's supervisor thread never panics (a panic there\r\n/// orphans the brain).\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub trait TrialEnv {\r\n    /// The current two-phase applied record, or `None` for no pending trial\r\n    /// (absent/corrupt degrades to `None`).\r\n    fn applied_state(&self) -> Option<AppliedRecord>;\r\n    /// The generation stamped in `brain.ready`, or `None` if absent/garbled\r\n    /// (the readiness gate reads `None` as not-ready — fail-safe).\r\n    fn ready_generation(&self) -> Option<u64>;\r\n    /// Delete `brain.ready` immediately before a trial spawn (**A9** — close the\r\n    /// cross-lifetime staleness hole: the in-memory generation resets to 0 on a\r\n    /// broker restart, so a stale gen-0 file would otherwise match a fresh gen-0\r\n    /// trial and falsely promote a brain that never booted).\r\n    fn clear_ready(&self);\r\n    /// Promote the candidate: it reached ready. Persist `Applied{version}` plus\r\n    /// the legacy `applied.json`/`last-outcome=applied` convergence records\r\n    /// (relocated here from `apply`, where they were the optimistic enlyzeam\r\n    /// write — promotion is the *real* moment the new code is accepted).\r\n    fn record_promoted(&self, version: u64);\r\n    /// Correct the record: the candidate failed readiness. Persist\r\n    /// `RolledBack{…}` and fire the loud, resurfacing rollback notif.\r\n    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);\r\n    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is\r\n    /// actually running), or `None` if absent/garbled — degrades the promotion\r\n    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\r\n    fn ready_exe_hash(&self) -> Option<String>;\r\n    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\r\n    /// the candidate SHOULD be running), or `None` if no staged metadata /\r\n    /// version mismatch / platform absent — degrades the gate to readiness-only.\r\n    fn staged_artifact_hash(&self, version: u64) -> Option<String>;\r\n    /// Whether the OLD brain generation's broker subscriber connection is DRAINED —\r\n    /// its local controller conn CLOSED or stall-evicted, no blocked writes still\r\n    /// pending on it (REQ-UPDATE-PROMOTE-DRAINED). Reads BROKER TRUTH in-process (no\r\n    /// brain round-trip): the supervisor promotes a ready candidate only once this is\r\n    /// `true`, so `brain.ready` arriving inside the W2 `BRAIN_WRITE_DEADLINE` window\r\n    /// can no longer promote onto a still-frozen control plane (the 22:47 false-\r\n    /// promote). Fail-safe toward DRAINED (`true`) where there is nothing to read — a\r\n    /// net-less broker, no hosted sessions, or [`NoTrialEnv`] — so the gate never\r\n    /// strands an apply where no old-gen control plane exists.\r\n    fn old_gen_drained(&self) -> bool;\r\n}\r\n\r\n/// The no-trial environment: every probe degrades to \"nothing pending / not\r\n/// ready\" and every effect is a no-op. Used where there is no readiness gate to\r\n/// drive — the supervisor then behaves exactly as the pre-D6 respawn loop.\r\npub struct NoTrialEnv;\r\n\r\nimpl TrialEnv for NoTrialEnv {\r\n    fn applied_state(&self) -> Option<AppliedRecord> {\r\n        None\r\n    }\r\n…\n}","startLine":159,"lineNumbers":[159,null,259,null,264,null,266,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,null,512]},"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":12585,"outputLines":225,"outputBytes":12585,"shownRange":{"start":269,"end":493},"nextOffset":494}}},"isError":false,"timestamp":1784195928082}}
{"type":"message","id":"f65af961","parentId":"77744176","timestamp":"2026-07-16T09:58:48.104Z","message":{"role":"toolResult","toolCallId":"call_cNDcoYboRGAGiF0K2VfhdANm|fc_07547ad47f060e5d016a58ab577670819aa01d75e903add77b","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## brainproc.rs\n 1110|    let canonical_exe = std::env::current_exe().ok();\n*1111|    let handle = thread::spawn(move || {\n 1112|        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n 1113|        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n 1114|        // readiness-only (no old-gen control plane to drain).\n## dispatch.rs\n 14|//! The stream table holds locally-opened outbound streams too. The dispatcher\n*15|//! claims only rows with `initiated_locally == false` — serving our own\n 16|//! requester stream would deadlock the requester against ourselves. The flag\n 17|//! is broker-recorded at registration (`open_stream` vs the accept loop),\n 18|//! never inferred.\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 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 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...\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 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...\n 690|) -> thread::JoinHandle<()> {\n*691|    thread::spawn(move || {\n 692|        let paths = DispatchPaths::production();\n 693|        if let Err(e) =\n 694|            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n## nethost.rs\n 121|/// (a remote always presents its own key and cannot claim to be us). This is\n*122|/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12\n 123|/// W1.5, B1) — QUIC self-dial is refused by iroh (\"Connecting to ourself is not\n 124|/// supported\"), so local attach rides a broker loopback conn instead.\n 125|enum ConnKind {\n...\n 414|/// A stream's send half — the same two transports as [`ConnKind`]. The\n*415|/// `serve_attach`/operator pump and the `StreamLog` read discipline are\n 416|/// IDENTICAL across both; only this leaf write differs (QUIC stream vs the\n 417|/// write end of an in-process duplex). That is the M12 W1.5 one-pump invariant\n 418|/// made structural: a loopback attach and a cross-node attach diverge at this\n...\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...\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...\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...\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 856|        runtime.spawn(async move {\n*857|            while let Some(incoming) = ep.accept().await {\n 858|                let sh = Arc::clone(&sh);\n 859|                let rate = Arc::clone(&rate);\n 860|                let membership = accept_membership.clone();\n...\n 866|                tokio::spawn(async move {\n*867|                    let Ok(connecting) = incoming.accept() else {\n 868|                        return;\n 869|                    };\n 870|                    let Ok(conn) = connecting.await else { return };\n...\n 1237|    /// broker-minted BY CONSTRUCTION and can never arise from an inbound dial.\n*1238|    /// This is what lets a LOCAL `spt rc` ride the byte-identical `serve_attach`\n 1239|    /// pump — QUIC self-dial is refused by iroh, so local attach uses this.\n 1240|    /// Idempotent: every call returns the same singleton conn (one is all a\n 1241|    /// local attach needs); no journaling needed (reuse is the dedup).\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 1273|    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,\n...\n 1328|                // before the operator can write its Request onto the wire — the\n*1329|                // dispatcher must never miss the stream.\n 1330|                register_stream(\n 1331|                    &shared,\n 1332|                    conn_id,\n...\n 1420|                    finished: log.finished,\n*1421|                    initiated_locally: e.initiated_locally,\n 1422|                }\n 1423|            })\n 1424|            .collect()\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 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 1791|            let sent = Arc::clone(&sent);\n*1792|            std::thread::spawn(move || {\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## broker.rs\n 57|    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n*58|    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\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...\n 941|        let writer_blocked = Arc::clone(&write_blocked_since);\n*942|        let writer = thread::spawn(move || {\n 943|            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n 944|        });\n 945|        self.controller = Some(ControllerSink {\n...\n 1130|        let writer =\n*1131|            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n 1132|        self.viewers.insert(\n 1133|            vid,\n 1134|            ViewerSink {\n...\n 1300|    /// broker is the single writer — resolves the clear-race). The remote-drive\n*1301|    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced\n 1302|    /// operator's teardown can never wipe the new controller's marker.\n 1303|    /// Best-effort: a session served without a perch (raw broker tests) has\n 1304|    /// nowhere to record.\n...\n 1745|        let (tx, rx) = sync_channel::<InputRecord>(input_queue_depth());\n*1746|        let writer = thread::spawn(move || input_writer(session, rx));\n 1747|        Arc::new(InputWriter {\n 1748|            tx,\n 1749|            backpressure: AtomicBool::new(false),\n...\n 1947|            let sup_endpoint = endpoint_id.to_string();\n*1948|            thread::spawn(move || {\n 1949|                run_inject_worker_supervised(\n 1950|                    sup_child,\n 1951|                    sup_faulted,\n...\n 2750|            let me = Arc::clone(self);\n*2751|            thread::spawn(move || {\n 2752|                let _ = me.handle_conn(conn);\n 2753|            });\n 2754|        }\n...\n 2773|        let me = Arc::clone(self);\n*2774|        Ok(thread::spawn(move || {\n 2775|            let _ = me.handle_conn(conn);\n 2776|        }))\n 2777|    }\n...\n 2834|        // stays un-reapable as a live-but-wedged writer even under child output.\n*2835|        let writer = thread::spawn(move || for _ in rx.iter() {});\n 2836|        recover_log(&log).controller = Some(ControllerSink {\n 2837|            tx,\n 2838|            send,\n...\n 3086|                KIND_NET_STREAM_OPEN => {\n*3087|                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n 3088|                        send_error(&send, &msg);\n 3089|                    }\n 3090|                }\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 3096|                KIND_NET_STREAMS => {\n*3097|                    if let Err(msg) = self.dispatch_net_streams(&send) {\n 3098|                        send_error(&send, &msg);\n 3099|                    }\n 3100|                }\n...\n 3180|                }\n*3181|                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n 3182|                    Ok(id) => my_stream_subs.push(id),\n 3183|                    Err(msg) => send_error(&send, &msg),\n 3184|                },\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 3483|    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n*3484|    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n 3485|    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\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...\n 4196|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*4197|    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4198|        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\n 4199|            .map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;\n 4200|        let host = self.net.get().ok_or(\"net disabled\")?;\n...\n 4203|                KIND_NET_STREAM_OPENED,\n*4204|                serde_json::to_value(NetStreamOpened {\n 4205|                    stream_id,\n 4206|                    conn_id,\n 4207|                    op_id,\n...\n 4209|                })\n*4210|                .expect(\"NetStreamOpened serializes\"),\n 4211|            )\n 4212|        };\n 4213|        match req.op_id {\n...\n 4251|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*4252|    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4253|        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n 4254|            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n 4255|        let bytes =\n...\n 4291|    /// brain uses to find streams that arrived while it was down.\n*4292|    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\n 4293|        let host = self.net.get().ok_or(\"net disabled\")?;\n 4294|        let frame = Envelope::new(\n 4295|            KIND_NET_STREAMS_REPLY,"}],"details":{"scopePath":"crates/spt-daemon/src/brainproc.rs, crates/spt-daemon/src/dispatch.rs, 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":49,"fileCount":4,"files":["crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":1},{"path":"crates/spt-daemon/src/dispatch.rs","count":10},{"path":"crates/spt-daemon/src/nethost.rs","count":18},{"path":"crates/spt-daemon/src/broker.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## brainproc.rs\n 1110│    let canonical_exe = std::env::current_exe().ok();\n*1111│    let handle = thread::spawn(move || {\n 1112│        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n 1113│        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n 1114│        // readiness-only (no old-gen control plane to drain).\n## dispatch.rs\n  14│//! The stream table holds locally-opened outbound streams too. The dispatcher\n *15│//! claims only rows with `initiated_locally == false` — serving our own\n  16│//! requester stream would deadlock the requester against ourselves. The flag\n  17│//! is broker-recorded at registration (`open_stream` vs the accept loop),\n  18│//! never inferred.\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 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 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    │...\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 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    │...\n 690│) -> thread::JoinHandle<()> {\n*691│    thread::spawn(move || {\n 692│        let paths = DispatchPaths::production();\n 693│        if let Err(e) =\n 694│            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n## nethost.rs\n  121│/// (a remote always presents its own key and cannot claim to be us). This is\n *122│/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12\n  123│/// W1.5, B1) — QUIC self-dial is refused by iroh (\"Connecting to ourself is not\n  124│/// supported\"), so local attach rides a broker loopback conn instead.\n  125│enum ConnKind {\n     │...\n  414│/// A stream's send half — the same two transports as [`ConnKind`]. The\n *415│/// `serve_attach`/operator pump and the `StreamLog` read discipline are\n  416│/// IDENTICAL across both; only this leaf write differs (QUIC stream vs the\n  417│/// write end of an in-process duplex). That is the M12 W1.5 one-pump invariant\n  418│/// made structural: a loopback attach and a cross-node attach diverge at this\n     │...\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     │...\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     │...\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     │...\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  856│        runtime.spawn(async move {\n *857│            while let Some(incoming) = ep.accept().await {\n  858│                let sh = Arc::clone(&sh);\n  859│                let rate = Arc::clone(&rate);\n  860│                let membership = accept_membership.clone();\n     │...\n  866│                tokio::spawn(async move {\n *867│                    let Ok(connecting) = incoming.accept() else {\n  868│                        return;\n  869│                    };\n  870│                    let Ok(conn) = connecting.await else { return };\n     │...\n 1237│    /// broker-minted BY CONSTRUCTION and can never arise from an inbound dial.\n*1238│    /// This is what lets a LOCAL `spt rc` ride the byte-identical `serve_attach`\n 1239│    /// pump — QUIC self-dial is refused by iroh, so local attach uses this.\n 1240│    /// Idempotent: every call returns the same singleton conn (one is all a\n 1241│    /// local attach needs); no journaling needed (reuse is the dedup).\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 1273│    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,\n     │...\n 1328│                // before the operator can write its Request onto the wire — the\n*1329│                // dispatcher must never miss the stream.\n 1330│                register_stream(\n 1331│                    &shared,\n 1332│                    conn_id,\n     │...\n 1420│                    finished: log.finished,\n*1421│                    initiated_locally: e.initiated_locally,\n 1422│                }\n 1423│            })\n 1424│            .collect()\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 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 1791│            let sent = Arc::clone(&sent);\n*1792│            std::thread::spawn(move || {\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## broker.rs\n   57│    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n  *58│    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\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     │...\n  941│        let writer_blocked = Arc::clone(&write_blocked_since);\n *942│        let writer = thread::spawn(move || {\n  943│            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n  944│        });\n  945│        self.controller = Some(ControllerSink {\n     │...\n 1130│        let writer =\n*1131│            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n 1132│        self.viewers.insert(\n 1133│            vid,\n 1134│            ViewerSink {\n     │...\n 1300│    /// broker is the single writer — resolves the clear-race). The remote-drive\n*1301│    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced\n 1302│    /// operator's teardown can never wipe the new controller's marker.\n 1303│    /// Best-effort: a session served without a perch (raw broker tests) has\n 1304│    /// nowhere to record.\n     │...\n 1745│        let (tx, rx) = sync_channel::<InputRecord>(input_queue_depth());\n*1746│        let writer = thread::spawn(move || input_writer(session, rx));\n 1747│        Arc::new(InputWriter {\n 1748│            tx,\n 1749│            backpressure: AtomicBool::new(false),\n     │...\n 1947│            let sup_endpoint = endpoint_id.to_string();\n*1948│            thread::spawn(move || {\n 1949│                run_inject_worker_supervised(\n 1950│                    sup_child,\n 1951│                    sup_faulted,\n     │...\n 2750│            let me = Arc::clone(self);\n*2751│            thread::spawn(move || {\n 2752│                let _ = me.handle_conn(conn);\n 2753│            });\n 2754│        }\n     │...\n 2773│        let me = Arc::clone(self);\n*2774│        Ok(thread::spawn(move || {\n 2775│            let _ = me.handle_conn(conn);\n 2776│        }))\n 2777│    }\n     │...\n 2834│        // stays un-reapable as a live-but-wedged writer even under child output.\n*2835│        let writer = thread::spawn(move || for _ in rx.iter() {});\n 2836│        recover_log(&log).controller = Some(ControllerSink {\n 2837│            tx,\n 2838│            send,\n     │...\n 3086│                KIND_NET_STREAM_OPEN => {\n*3087│                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n 3088│                        send_error(&send, &msg);\n 3089│                    }\n 3090│                }\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 3096│                KIND_NET_STREAMS => {\n*3097│                    if let Err(msg) = self.dispatch_net_streams(&send) {\n 3098│                        send_error(&send, &msg);\n 3099│                    }\n 3100│                }\n     │...\n 3180│                }\n*3181│                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n 3182│                    Ok(id) => my_stream_subs.push(id),\n 3183│                    Err(msg) => send_error(&send, &msg),\n 3184│                },\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 3483│    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n*3484│    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n 3485│    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\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     │...\n 4196│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*4197│    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4198│        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\n 4199│            .map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;\n 4200│        let host = self.net.get().ok_or(\"net disabled\")?;\n     │...\n 4203│                KIND_NET_STREAM_OPENED,\n*4204│                serde_json::to_value(NetStreamOpened {\n 4205│                    stream_id,\n 4206│                    conn_id,\n 4207│                    op_id,\n     │...\n 4209│                })\n*4210│                .expect(\"NetStreamOpened serializes\"),\n 4211│            )\n 4212│        };\n 4213│        match req.op_id {\n     │...\n 4251│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*4252│    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4253│        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n 4254│            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n 4255│        let bytes =\n     │...\n 4291│    /// brain uses to find streams that arrived while it was down.\n*4292│    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\n 4293│        let host = self.net.get().ok_or(\"net disabled\")?;\n 4294│        let frame = Envelope::new(\n 4295│            KIND_NET_STREAMS_REPLY,"},"isError":false,"timestamp":1784195928104}}
{"type":"message","id":"3dcbd77a","parentId":"f65af961","timestamp":"2026-07-16T09:58:48.132Z","message":{"role":"toolResult","toolCallId":"call_VKnitMU6LarZ7sr37zMVXAND|fc_07547ad47f060e5d016a58ab5777c4819aac042ac7ac6a2054","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/tests/\n### access.rs\n 220|    // (3) Whitelist node A; its unsolicited traffic now passes — the gate\n*221|    // re-read the store mid-stream (no restart, no resubscribe).\n 222|    let mut store = AccessStore::load();\n 223|    store.allow(&target, &a_node);\n 224|    store.save().expect(\"save access store\");\n### attach.rs\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...\n 771|\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 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 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 865|        let outcome = serve_attach(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\n*866|            .expect(\"re-serve\");\n 867|        (outcome, life2)\n 868|    });\n 869|\n...\n 891|    // the operator's ENTIRE received byte stream (early live frames + the\n*892|    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\n 893|    // the resulting authoritative screen still shows BOTH markers. The screen is the\n 894|    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n 895|    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n*896|    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n 897|    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n 898|    // makes this robust to the mid-stream repaint's clear/repaint bytes.\n 899|    let mut screen = ScreenGrid::new(24, 80);\n...\n 904|        \"the operator's rendered screen still shows the pre-crash marker across the \\\n*905|         brain restart (no gap in the re-served output): {painted:?}\"\n 906|    );\n 907|    assert!(\n 908|        painted.contains(\"MARKER_TWO\"),\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...\n 1310|// a viewer are both attached when the controller's serving brain RESTARTS (its\n*1311|// connection drops; a successor re-serves with the SAME operator identity). The\n 1312|// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\n 1313|// the VIEWER survives the churn and keeps receiving output. This is the self-kick\n 1314|// guard ∩ viewer survival, the one scenario the separate tests left uncombined.\n...\n 1339|\n*1340|    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\n 1341|    // re-take. It silently becomes controller (NEVER displacing anyone): the\n 1342|    // gate-#7 self-kick guard on a real controller-conn restart.\n 1343|    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\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...\n 1417|    // re-subscribes from the resume cursor (the gapless-resume mechanism). The\n*1418|    // reorder safety for a later `serve_attach` re-serve over this same socket is\n 1419|    // the broker's epoch gate, not the absence of this subscribe.\n 1420|    let succ = Brain::handoff(&name, state, 2).expect(\"handoff\");\n 1421|\n...\n 1428|        \"handoff seeds session_cursors at prior.next_seq (resume-mode dedup floor) — \\\n*1429|         this is what keeps a re-served at-least-once boundary re-send from becoming \\\n 1430|         a fatal `output gap` (REQ-HAZARD-CONTROLLER-WRITER-REORDER fix #3)\"\n 1431|    );\n 1432|    // The brain is in RESUME mode (the per-session map is populated), which is the\n...\n 1468|//\n*1469|// THE BUG it guards: on a re-serve, `serve_attach`'s brain CONSUMES the handoff\n 1470|// replay's frame at seq=K BEFORE it is `attached` (the frame is dropped by the\n 1471|// `if attached` forward gate, but the snap-above cursor has advanced past K).\n 1472|// `attach_as(sid, 0)`'s re-subscribe must then RESET the dedup cursor to 0 — else\n...\n 1529|\n*1530|    // THE FIX UNDER GUARD: the re-serve subscribe from 0 (serve_attach's\n 1531|    // `attach_as(sid, 0)`) MUST reset the dedup cursor to from_seq=0, so the\n 1532|    // broker's re-send of [0,K] is re-delivered (the operator's render cursor\n 1533|    // dedups the overlap) and seq K reaches the viewport. A revert of the\n...\n 1536|    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"op-node\"))\n*1537|        .expect(\"re-serve subscribe\");\n 1538|    assert_eq!(\n 1539|        succ.session_cursor(sid),\n 1540|        Some(0),\n...\n 1542|         (Brain::subscribe_with) even after a pre-attached consume advanced it to {advanced} — \\\n*1543|         else the operator-stream re-serve drops the already-consumed seq K \\\n 1544|         (REQ-HAZARD-CONTROLLER-WRITER-REORDER, the kitsubito operator-stream boundary)\"\n 1545|    );\n 1546|\n### broker.rs\n 977|#[test]\n*978|fn exactly_once_preserved_on_no_ack_path() {\n 979|    exactly_once_no_ack_test();\n 980|}\n 981|\n...\n 985|// from_seq=1, then re-subscribes from_seq=0 over the SAME conn — reproducing the\n*986|// pre-fix handoff(eager subscribe @ prior.next_seq=1) + re-serve(attach_as(sid,0))\n 987|// sequence that spawned writer-A then writer-B racing one socket). The fix makes\n 988|// the operator/consumer view monotonic + byte-exact + exactly-once, and the\n 989|// session stays live. Real broker + real PTY + real local IPC — NO mocks.\n...\n 1113|    //   (2) subscribe from_seq=0  → broker become_controller(from_seq=0), initial=[0..]\n*1114|    //       bumps controller_epoch, spawns writer-B (the re-serve attach_as(sid,0)).\n 1115|    // Both `by:None`+Control = SAME identity → each is a silent re-take (gate #7),\n*1116|    // exactly the brain-restart re-serve shape. Pre-fix writer-A's stale seq 1 can\n 1117|    // win the socket ahead of writer-B's seq 0 → reorder.\n 1118|    for floor in [1u64, 0u64] {\n 1119|        send(\n...\n 1157|            std::time::Instant::now() < deadline,\n*1158|            \"liveness marker P1C_LIVE must arrive through the re-served controller \\\n 1159|             within the watchdog — a serve thread that died on a gap (the reorder \\\n 1160|             HANG face) never re-pumps it. accepted={:?}\",\n 1161|            dedup.accepted_seqs\n### budget.rs\n 11|//!\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### 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...\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...\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...\n 217|\n*218|    // ── 4. brain-restart-survives: kill+restart the brain repeatedly ────────────\n 219|    const RESTARTS: u64 = 10;\n 220|    // Inject the first successor marker, then crash (drop the connection).\n 221|    brain\n### daemon_lifecycle_real_brain.rs\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...\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### inject_control_wedge.rs\n 1724|/// F029 C-1: under the default N=3 strike budget a single miss is NOT a fault (the\n*1725|/// binary is preserved), but the respool-once STILL fires on that first miss —\n 1726|/// `respool_envelope` writes the envelope to the endpoint's perch spool so the\n 1727|/// relay/poll can still surface it. This gate asserts that respool-once; it holds\n 1728|/// whether or not the miss escalates to a fault (the pinned budget=1 makes it\n...\n 1849|// [int->REQ-TRANSLATE-COMMIT-MISS-TOLERANCE]\n*1850|fn c1_miss_once_preserves_binary_and_next_inject_delivers() {\n 1851|    init_wedge_home();\n 1852|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 1853|    let stdin_log = dir.path().join(\"c1a-stdin.log\");\n...\n 1869|    // Event 1 MISSES its {commit} (nocommit_once). Under C-1 this must NOT fault:\n*1870|    // the binary is preserved, the floor releases, the envelope respools once.\n 1871|    let ev1 = br#\"<EVENT type=\"message\" from=\"peer\">c1a-first-miss</EVENT>\"#;\n 1872|    let _ = deliver_endpoint_event(&name, endpoint, ev1, Duration::from_secs(4));\n 1873|    // Let the 400ms commit deadline elapse (the miss is handled) before event 2.\n### input_ack_deadlock.rs\n 21|//! the flood → the return direction can't fill → the handler always drains → no\n*22|//! deadlock. Exactly-once is preserved (dedup at the broker's applied-set,\n 23|//! independent of the ack). `shellchan` keeps `send_effect` (ack=true) and its\n 24|//! `Applied`-wait. THIS test asserts the fixed behavior.\n 25|//!\n### mesh.rs\n 432|// directly, with B as a non-relaying hub (B emits only its OWN rows, so an\n*433|// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).\n 434|// The widened push (Mesh-D6) makes each spoke push its own rows to every roster\n 435|// member directly; B emits only its own rows, so it never forwards a third party.\n 436|// [int->REQ-MESH-3]\n### netbroker.rs\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 239|// key stays un-applied and the conn table stays empty — so a fresh op re-dials\n 240|// cleanly (no dedupe into a dead conn). And the broker keeps serving after: it\n 241|// REPLIED an error, it did not wedge.\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### 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### propagate.rs\n 430|// [unit->REQ-UPD-6] update-set propagation selects the recipient's Rust target\n*431|// triple, fetches only that artifact, and then can re-serve that same platform\n 432|// artifact from the recipient's partial staged set.\n 433|#[test]\n 434|fn update_set_propagates_selected_platform_artifact() {\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### psyche_residency_expectation_e2e.rs\n 13|//! STRIKE PATH, NEVER RESEED (explicit): the failing role exits **3** — a plain strike,\n*14|//! NOT the reserved session-not-found code **95**. So custody is KEPT across every\n 15|//! fire (asserted below: the minted psyche sid persists, `PSYCHE_RESEED` never fires).\n 16|//! The conformance int exercises the STRIKE budget, never the reseed path.\n 17|//!\n...\n 70|// arg (the filled `{parent_session_id}` = [`ARGV_MARKER`]) as one proof line, then EXIT\n*71|// 3 (a plain strike, NOT the reserved reseed code 95). Reading a SINGLE positional is\n 72|// load-bearing: a whitespace-split fill makes `$1` / `%~2` see only \"alpha\".\n 73|//   unix: `sh -c '<script>' _ {parent_session_id}` — `$1` is the marker token.\n 74|//   win:  a one-shot `.bat` (written by the test) — `%~2` is the marker (quote-stripped);\n...\n 84|    // appended VERBATIM (`printf %s`) so each proof line equals the whole multi-word marker.\n*85|    // `exit 3` is a plain strike (NOT the reserved reseed code 95).\n 86|    let sh = home.join(\"mock-fail-resume.sh\");\n 87|    std::fs::write(&sh, \"printf '%s\\\\n' \\\"$2\\\" >> \\\"$1\\\"\\nexit 3\\n\").unwrap();\n 88|    format!(\"sh {} {proof} {{parent_session_id}}\", sh.display())\n### sync.rs\n 346|\n*347|// [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a partition's concurrent writes\n 348|// surface as durable artifacts on BOTH nodes; the non-elected node stands\n 349|// down; the elected node's bounded Psyche turn mints a dominating merge; and\n 350|// the merge propagates as a plain accept that clears artifacts everywhere —\n...\n 497|    // The merge propagates to A as a plain accept and clears A's artifacts —\n*498|    // both versions were preserved end-to-end, and the subnet converges.\n 499|    let (rep_a2, _) = a_from_b.pull(&[\"a-doyle\"], &policy_b);\n 500|    assert!(\n 501|        rep_a2.applied[0]\n### two_origin_spanning.rs\n 7|//! - the **echo-commune** reads them **opaque + verbatim** through `[history]`\n*8|//!   (full fidelity, extra adapter fields preserved), while\n 9|//! - the **digest** reads them **contract-typed** through its own `[digest]`\n 10|//!   extractor (REQ-TERM-5) —\n 11|//!\n...\n 32|// A stdin pass-through: the source bytes round-trip to stdout. As a `[history]`\n*33|// normalizer it preserves full fidelity (echo's opaque needs); as a `[digest]`\n 34|// extractor over a contract-shaped source it is the trivial identity extractor.\n 35|#[cfg(unix)]\n 36|const PASSTHROUGH: &str = \"cat\";\n### xfer.rs\n 13|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): the target brain\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...\n 224|\n*225|// [unit->REQ-REACH-1] push survives a target-brain crash mid-receive exactly\n*226|// once: life 1 dies with a partial temp file on disk; the successor re-serves\n 227|// the SAME broker-held stream from seq 0 — the replayed Push re-opens the\n 228|// temp, the replayed chunks rewrite the same bytes at the same offsets\n 229|// (positional idempotency), the commit renames atomically, and the operator's\n...\n 232|#[test]\n*233|fn push_survives_target_brain_restart_exactly_once() {\n 234|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 235|    let (name_a, name_b) = (unique_name(), unique_name());\n 236|    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\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*328|        serve_xfer(&mut life2, stream_a, \"ling\", &origin, 0, &root, &progress_a).expect(\"re-serve\");\n 329|    assert_eq!(outcome, XferServeOutcome::Received { bytes: total });\n 330|\n 331|    // The operator's blocked push observed the durable commit.\n\n## spt/tests/\n### adapter_post_step.rs\n 9|//! arm of the stdout notice arbitration: custom text SUPERSEDES `[update].message`;\n*10|//! the reserved sentinel FIRES the static `[update].message`; empty = no notice; a\n 11|//! nonzero exit is loud (overall exit nonzero) and failure-isolated (no rollback —\n 12|//! there is nothing to roll back on a no-op, and the post path never touches the\n 13|//! committed pull).\n...\n 99|\n*100|    // ── sentinel: the reserved token fires the static [update].message. ──\n 101|    let _ = std::fs::remove_file(&seam_out);\n 102|    let out = run_update(&spt_bin, home.path(), \"sentinel\", &seam_out);\n 103|    let stdout = String::from_utf8_lossy(&out.stdout);\n### bind_adapter_profile_persist_e2e.rs\n 86|#[test]\n*87|fn bind_over_created_profile_endpoint_preserves_the_profile() {\n 88|    let home = tempfile::tempdir().unwrap();\n 89|    std::env::set_var(\"SPT_HOME\", home.path());\n 90|    start_inproc_daemon();\n...\n 176|        Some(created_adapter),\n*177|        \"the bare-parent hook-bind must PRESERVE the created `{created_adapter}` — a \\\n 178|         profile-agnostic re-bind must never clobber it to `{parent_only}` (the A-4 \\\n 179|         field regression). Got {adapter_after:?}\"\n 180|    );\n### brain_split.rs\n 5|//! always-up per-machine anchor that spawns + supervises a `spt daemon brain`\n*6|//! child, and **the broker survives the brain dying** — the supervisor respawns\n 7|//! the brain, which reconnects to the still-serving broker socket. A respawned\n 8|//! brain (a NEW pid in `brain.ready`) can only appear if the broker stayed alive\n 9|//! to accept its connection, so the respawn IS the broker-survival proof.\n...\n 105|#[test]\n*106|fn seed_anchor_survives_brain_cycle() {\n 107|    let home = tempfile::tempdir().unwrap();\n 108|    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n 109|    let ready = home.path().join(\"brain.ready\");\n...\n 169|#[test]\n*170|fn broker_survives_brain_kill_and_respawns_it() {\n 171|    let home = tempfile::tempdir().unwrap();\n 172|    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n 173|    let ready = home.path().join(\"brain.ready\");\n\nShowing files 1-20 of 77+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt/tests, docs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":62,"fileCount":20,"files":["crates/spt-daemon/tests/access.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/budget.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/mesh.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/netstream.rs","crates/spt-daemon/tests/presence.rs","crates/spt-daemon/tests/propagate.rs","crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","crates/spt-daemon/tests/sync.rs","crates/spt-daemon/tests/two_origin_spanning.rs","crates/spt-daemon/tests/xfer.rs","crates/spt/tests/adapter_post_step.rs","crates/spt/tests/bind_adapter_profile_persist_e2e.rs","crates/spt/tests/brain_split.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/access.rs","count":1},{"path":"crates/spt-daemon/tests/attach.rs","count":19},{"path":"crates/spt-daemon/tests/broker.rs","count":5},{"path":"crates/spt-daemon/tests/budget.rs","count":1},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":4},{"path":"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","count":2},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":3},{"path":"crates/spt-daemon/tests/input_ack_deadlock.rs","count":1},{"path":"crates/spt-daemon/tests/mesh.rs","count":1},{"path":"crates/spt-daemon/tests/netbroker.rs","count":1},{"path":"crates/spt-daemon/tests/netstream.rs","count":1},{"path":"crates/spt-daemon/tests/presence.rs","count":1},{"path":"crates/spt-daemon/tests/propagate.rs","count":2},{"path":"crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","count":3},{"path":"crates/spt-daemon/tests/sync.rs","count":2},{"path":"crates/spt-daemon/tests/two_origin_spanning.rs","count":2},{"path":"crates/spt-daemon/tests/xfer.rs","count":6},{"path":"crates/spt/tests/adapter_post_step.rs","count":2},{"path":"crates/spt/tests/bind_adapter_profile_persist_e2e.rs","count":2},{"path":"crates/spt/tests/brain_split.rs","count":3}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/tests/\n### access.rs\n 220│    // (3) Whitelist node A; its unsolicited traffic now passes — the gate\n*221│    // re-read the store mid-stream (no restart, no resubscribe).\n 222│    let mut store = AccessStore::load();\n 223│    store.allow(&target, &a_node);\n 224│    store.save().expect(\"save access store\");\n### attach.rs\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     │...\n  771│\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  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  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  865│        let outcome = serve_attach(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\n *866│            .expect(\"re-serve\");\n  867│        (outcome, life2)\n  868│    });\n  869│\n     │...\n  891│    // the operator's ENTIRE received byte stream (early live frames + the\n *892│    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\n  893│    // the resulting authoritative screen still shows BOTH markers. The screen is the\n  894│    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n  895│    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n *896│    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n  897│    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n  898│    // makes this robust to the mid-stream repaint's clear/repaint bytes.\n  899│    let mut screen = ScreenGrid::new(24, 80);\n     │...\n  904│        \"the operator's rendered screen still shows the pre-crash marker across the \\\n *905│         brain restart (no gap in the re-served output): {painted:?}\"\n  906│    );\n  907│    assert!(\n  908│        painted.contains(\"MARKER_TWO\"),\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     │...\n 1310│// a viewer are both attached when the controller's serving brain RESTARTS (its\n*1311│// connection drops; a successor re-serves with the SAME operator identity). The\n 1312│// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\n 1313│// the VIEWER survives the churn and keeps receiving output. This is the self-kick\n 1314│// guard ∩ viewer survival, the one scenario the separate tests left uncombined.\n     │...\n 1339│\n*1340│    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\n 1341│    // re-take. It silently becomes controller (NEVER displacing anyone): the\n 1342│    // gate-#7 self-kick guard on a real controller-conn restart.\n 1343│    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\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     │...\n 1417│    // re-subscribes from the resume cursor (the gapless-resume mechanism). The\n*1418│    // reorder safety for a later `serve_attach` re-serve over this same socket is\n 1419│    // the broker's epoch gate, not the absence of this subscribe.\n 1420│    let succ = Brain::handoff(&name, state, 2).expect(\"handoff\");\n 1421│\n     │...\n 1428│        \"handoff seeds session_cursors at prior.next_seq (resume-mode dedup floor) — \\\n*1429│         this is what keeps a re-served at-least-once boundary re-send from becoming \\\n 1430│         a fatal `output gap` (REQ-HAZARD-CONTROLLER-WRITER-REORDER fix #3)\"\n 1431│    );\n 1432│    // The brain is in RESUME mode (the per-session map is populated), which is the\n     │...\n 1468│//\n*1469│// THE BUG it guards: on a re-serve, `serve_attach`'s brain CONSUMES the handoff\n 1470│// replay's frame at seq=K BEFORE it is `attached` (the frame is dropped by the\n 1471│// `if attached` forward gate, but the snap-above cursor has advanced past K).\n 1472│// `attach_as(sid, 0)`'s re-subscribe must then RESET the dedup cursor to 0 — else\n     │...\n 1529│\n*1530│    // THE FIX UNDER GUARD: the re-serve subscribe from 0 (serve_attach's\n 1531│    // `attach_as(sid, 0)`) MUST reset the dedup cursor to from_seq=0, so the\n 1532│    // broker's re-send of [0,K] is re-delivered (the operator's render cursor\n 1533│    // dedups the overlap) and seq K reaches the viewport. A revert of the\n     │...\n 1536│    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"op-node\"))\n*1537│        .expect(\"re-serve subscribe\");\n 1538│    assert_eq!(\n 1539│        succ.session_cursor(sid),\n 1540│        Some(0),\n     │...\n 1542│         (Brain::subscribe_with) even after a pre-attached consume advanced it to {advanced} — \\\n*1543│         else the operator-stream re-serve drops the already-consumed seq K \\\n 1544│         (REQ-HAZARD-CONTROLLER-WRITER-REORDER, the kitsubito operator-stream boundary)\"\n 1545│    );\n 1546│\n### broker.rs\n  977│#[test]\n *978│fn exactly_once_preserved_on_no_ack_path() {\n  979│    exactly_once_no_ack_test();\n  980│}\n  981│\n     │...\n  985│// from_seq=1, then re-subscribes from_seq=0 over the SAME conn — reproducing the\n *986│// pre-fix handoff(eager subscribe @ prior.next_seq=1) + re-serve(attach_as(sid,0))\n  987│// sequence that spawned writer-A then writer-B racing one socket). The fix makes\n  988│// the operator/consumer view monotonic + byte-exact + exactly-once, and the\n  989│// session stays live. Real broker + real PTY + real local IPC — NO mocks.\n     │...\n 1113│    //   (2) subscribe from_seq=0  → broker become_controller(from_seq=0), initial=[0..]\n*1114│    //       bumps controller_epoch, spawns writer-B (the re-serve attach_as(sid,0)).\n 1115│    // Both `by:None`+Control = SAME identity → each is a silent re-take (gate #7),\n*1116│    // exactly the brain-restart re-serve shape. Pre-fix writer-A's stale seq 1 can\n 1117│    // win the socket ahead of writer-B's seq 0 → reorder.\n 1118│    for floor in [1u64, 0u64] {\n 1119│        send(\n     │...\n 1157│            std::time::Instant::now() < deadline,\n*1158│            \"liveness marker P1C_LIVE must arrive through the re-served controller \\\n 1159│             within the watchdog — a serve thread that died on a gap (the reorder \\\n 1160│             HANG face) never re-pumps it. accepted={:?}\",\n 1161│            dedup.accepted_seqs\n### budget.rs\n 11│//!\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### 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    │...\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    │...\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    │...\n 217│\n*218│    // ── 4. brain-restart-survives: kill+restart the brain repeatedly ────────────\n 219│    const RESTARTS: u64 = 10;\n 220│    // Inject the first successor marker, then crash (drop the connection).\n 221│    brain\n### daemon_lifecycle_real_brain.rs\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   │...\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### inject_control_wedge.rs\n 1724│/// F029 C-1: under the default N=3 strike budget a single miss is NOT a fault (the\n*1725│/// binary is preserved), but the respool-once STILL fires on that first miss —\n 1726│/// `respool_envelope` writes the envelope to the endpoint's perch spool so the\n 1727│/// relay/poll can still surface it. This gate asserts that respool-once; it holds\n 1728│/// whether or not the miss escalates to a fault (the pinned budget=1 makes it\n     │...\n 1849│// [int->REQ-TRANSLATE-COMMIT-MISS-TOLERANCE]\n*1850│fn c1_miss_once_preserves_binary_and_next_inject_delivers() {\n 1851│    init_wedge_home();\n 1852│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 1853│    let stdin_log = dir.path().join(\"c1a-stdin.log\");\n     │...\n 1869│    // Event 1 MISSES its {commit} (nocommit_once). Under C-1 this must NOT fault:\n*1870│    // the binary is preserved, the floor releases, the envelope respools once.\n 1871│    let ev1 = br#\"<EVENT type=\"message\" from=\"peer\">c1a-first-miss</EVENT>\"#;\n 1872│    let _ = deliver_endpoint_event(&name, endpoint, ev1, Duration::from_secs(4));\n 1873│    // Let the 400ms commit deadline elapse (the miss is handled) before event 2.\n### input_ack_deadlock.rs\n 21│//! the flood → the return direction can't fill → the handler always drains → no\n*22│//! deadlock. Exactly-once is preserved (dedup at the broker's applied-set,\n 23│//! independent of the ack). `shellchan` keeps `send_effect` (ack=true) and its\n 24│//! `Applied`-wait. THIS test asserts the fixed behavior.\n 25│//!\n### mesh.rs\n 432│// directly, with B as a non-relaying hub (B emits only its OWN rows, so an\n*433│// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).\n 434│// The widened push (Mesh-D6) makes each spoke push its own rows to every roster\n 435│// member directly; B emits only its own rows, so it never forwards a third party.\n 436│// [int->REQ-MESH-3]\n### netbroker.rs\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 239│// key stays un-applied and the conn table stays empty — so a fresh op re-dials\n 240│// cleanly (no dedupe into a dead conn). And the broker keeps serving after: it\n 241│// REPLIED an error, it did not wedge.\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### 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### propagate.rs\n 430│// [unit->REQ-UPD-6] update-set propagation selects the recipient's Rust target\n*431│// triple, fetches only that artifact, and then can re-serve that same platform\n 432│// artifact from the recipient's partial staged set.\n 433│#[test]\n 434│fn update_set_propagates_selected_platform_artifact() {\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### psyche_residency_expectation_e2e.rs\n 13│//! STRIKE PATH, NEVER RESEED (explicit): the failing role exits **3** — a plain strike,\n*14│//! NOT the reserved session-not-found code **95**. So custody is KEPT across every\n 15│//! fire (asserted below: the minted psyche sid persists, `PSYCHE_RESEED` never fires).\n 16│//! The conformance int exercises the STRIKE budget, never the reseed path.\n 17│//!\n   │...\n 70│// arg (the filled `{parent_session_id}` = [`ARGV_MARKER`]) as one proof line, then EXIT\n*71│// 3 (a plain strike, NOT the reserved reseed code 95). Reading a SINGLE positional is\n 72│// load-bearing: a whitespace-split fill makes `$1` / `%~2` see only \"alpha\".\n 73│//   unix: `sh -c '<script>' _ {parent_session_id}` — `$1` is the marker token.\n 74│//   win:  a one-shot `.bat` (written by the test) — `%~2` is the marker (quote-stripped);\n   │...\n 84│    // appended VERBATIM (`printf %s`) so each proof line equals the whole multi-word marker.\n*85│    // `exit 3` is a plain strike (NOT the reserved reseed code 95).\n 86│    let sh = home.join(\"mock-fail-resume.sh\");\n 87│    std::fs::write(&sh, \"printf '%s\\\\n' \\\"$2\\\" >> \\\"$1\\\"\\nexit 3\\n\").unwrap();\n 88│    format!(\"sh {} {proof} {{parent_session_id}}\", sh.display())\n### sync.rs\n 346│\n*347│// [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a partition's concurrent writes\n 348│// surface as durable artifacts on BOTH nodes; the non-elected node stands\n 349│// down; the elected node's bounded Psyche turn mints a dominating merge; and\n 350│// the merge propagates as a plain accept that clears artifacts everywhere —\n    │...\n 497│    // The merge propagates to A as a plain accept and clears A's artifacts —\n*498│    // both versions were preserved end-to-end, and the subnet converges.\n 499│    let (rep_a2, _) = a_from_b.pull(&[\"a-doyle\"], &policy_b);\n 500│    assert!(\n 501│        rep_a2.applied[0]\n### two_origin_spanning.rs\n  7│//! - the **echo-commune** reads them **opaque + verbatim** through `[history]`\n *8│//!   (full fidelity, extra adapter fields preserved), while\n  9│//! - the **digest** reads them **contract-typed** through its own `[digest]`\n 10│//!   extractor (REQ-TERM-5) —\n 11│//!\n   │...\n 32│// A stdin pass-through: the source bytes round-trip to stdout. As a `[history]`\n*33│// normalizer it preserves full fidelity (echo's opaque needs); as a `[digest]`\n 34│// extractor over a contract-shaped source it is the trivial identity extractor.\n 35│#[cfg(unix)]\n 36│const PASSTHROUGH: &str = \"cat\";\n### xfer.rs\n  13│//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): the target brain\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    │...\n 224│\n*225│// [unit->REQ-REACH-1] push survives a target-brain crash mid-receive exactly\n*226│// once: life 1 dies with a partial temp file on disk; the successor re-serves\n 227│// the SAME broker-held stream from seq 0 — the replayed Push re-opens the\n 228│// temp, the replayed chunks rewrite the same bytes at the same offsets\n 229│// (positional idempotency), the commit renames atomically, and the operator's\n    │...\n 232│#[test]\n*233│fn push_survives_target_brain_restart_exactly_once() {\n 234│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 235│    let (name_a, name_b) = (unique_name(), unique_name());\n 236│    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\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*328│        serve_xfer(&mut life2, stream_a, \"ling\", &origin, 0, &root, &progress_a).expect(\"re-serve\");\n 329│    assert_eq!(outcome, XferServeOutcome::Received { bytes: total });\n 330│\n 331│    // The operator's blocked push observed the durable commit.\n\n## spt/tests/\n### adapter_post_step.rs\n   9│//! arm of the stdout notice arbitration: custom text SUPERSEDES `[update].message`;\n *10│//! the reserved sentinel FIRES the static `[update].message`; empty = no notice; a\n  11│//! nonzero exit is loud (overall exit nonzero) and failure-isolated (no rollback —\n  12│//! there is nothing to roll back on a no-op, and the post path never touches the\n  13│//! committed pull).\n    │...\n  99│\n*100│    // ── sentinel: the reserved token fires the static [update].message. ──\n 101│    let _ = std::fs::remove_file(&seam_out);\n 102│    let out = run_update(&spt_bin, home.path(), \"sentinel\", &seam_out);\n 103│    let stdout = String::from_utf8_lossy(&out.stdout);\n### bind_adapter_profile_persist_e2e.rs\n  86│#[test]\n *87│fn bind_over_created_profile_endpoint_preserves_the_profile() {\n  88│    let home = tempfile::tempdir().unwrap();\n  89│    std::env::set_var(\"SPT_HOME\", home.path());\n  90│    start_inproc_daemon();\n    │...\n 176│        Some(created_adapter),\n*177│        \"the bare-parent hook-bind must PRESERVE the created `{created_adapter}` — a \\\n 178│         profile-agnostic re-bind must never clobber it to `{parent_only}` (the A-4 \\\n 179│         field regression). Got {adapter_after:?}\"\n 180│    );\n### brain_split.rs\n   5│//! always-up per-machine anchor that spawns + supervises a `spt daemon brain`\n  *6│//! child, and **the broker survives the brain dying** — the supervisor respawns\n   7│//! the brain, which reconnects to the still-serving broker socket. A respawned\n   8│//! brain (a NEW pid in `brain.ready`) can only appear if the broker stayed alive\n   9│//! to accept its connection, so the respawn IS the broker-survival proof.\n    │...\n 105│#[test]\n*106│fn seed_anchor_survives_brain_cycle() {\n 107│    let home = tempfile::tempdir().unwrap();\n 108│    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n 109│    let ready = home.path().join(\"brain.ready\");\n    │...\n 169│#[test]\n*170│fn broker_survives_brain_kill_and_respawns_it() {\n 171│    let home = tempfile::tempdir().unwrap();\n 172│    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n 173│    let ready = home.path().join(\"brain.ready\");"},"isError":false,"timestamp":1784195928132}}
{"type":"message","id":"df4d1fd9","parentId":"3dcbd77a","timestamp":"2026-07-16T09:59:18.081Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing dispatch loop reconnection race**\n\n**Investigating dispatcher thread termination bug**\n\n**Tracing new dispatcher stream subscription flow**"},{"type":"thinking","thinking":"**Inspecting handle_conn cleanup and writer join**\n\n**Analyzing network stream and controller counts**"},{"type":"toolCall","id":"call_EE0RL5mNw6JaOG9rqkTLZS3C|fc_07547ad47f060e5d016a58ab74197c819abb2dd3f9eae156e5","name":"grep","arguments":{"i":"Finding four of five incident logs","pattern":"4/5|4 of 5|four of five|BRAIN_RESUMED|detach.*reattach|detach.*rc|rc.*detach|most.*session","path":"docs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_T6b3tTpBOa1BW1FmzeHUZ8pp|fc_07547ad47f060e5d016a58ab741990819ab32920122a383b57","name":"read","arguments":{"i":"Reading brain startup before loop","path":"crates/spt-daemon/src/brainproc.rs","selector":"170-320"}},{"type":"toolCall","id":"call_BraTxpgPGpafq9Fm1quCaRY9|fc_07547ad47f060e5d016a58ab74199c819ab1a520ea17ca4797","name":"read","arguments":{"i":"Reading attach serve subscription loop","path":"crates/spt-daemon/src/attach.rs","selector":"230-430"}},{"type":"toolCall","id":"call_9mEKtcZqGfub6Y7u6HrkDuPa|fc_07547ad47f060e5d016a58ab7419a4819ab458dd1b59bcc195","name":"read","arguments":{"i":"Reading broker connection cleanup","path":"crates/spt-daemon/src/broker.rs","selector":"3020-3225,3430-3515"}},{"type":"toolCall","id":"call_IFwWGGC3Y19es8MPlIMSbHGO|fc_07547ad47f060e5d016a58ab7419b0819ab2fcd8738a667624","name":"read","arguments":{"i":"Reading stream persistence table","path":"crates/spt-daemon/src/nethost.rs","selector":"1380-1430"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":21382,"output":1111,"cacheRead":60928,"cacheWrite":0,"totalTokens":83421,"reasoningTokens":857,"cost":{"input":0.10691,"output":0.03333,"cacheRead":0.030463999999999998,"cacheWrite":0,"total":0.170704}},"stopReason":"toolUse","timestamp":1784195928181,"responseId":"resp_07547ad47f060e5d016a58ab5887ac819ab2fc3e4eb209db68","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab5c177c819aa13a6d2174a7e4aa","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtoAZpZ6Oabt4sM-_cfqxdJuYwIjp9NUprTUfilfaLkP4qBzponJcSb__rLPiZi5UHUidhLM9gnApaSLS2TYGMZiwwfuaWwBdVXNynzqbVvapsWaX5vFmCEhPrbVZz9t6teCBLsZtuV12jy28v6yyBS8OgJkWFbt9-4JVMtcbXTrm68crRGyKGjFG3SAItTaS5HVmMPmkm8rWKxXk4_hCdfAGnbFWoDu3rjnwqegtQJ753SGJ6ygGNBIk0ctIi0I07pziVZ32vlrwGDj84t31lAph0P0m126gL6JrFV5s5y9K9JdnSjLRaStJjc6MNzEN1vgg1dFI0Ts2jotE03JG0XYDFByIrOUwUeDmGlT_PMr94Tcvz7tXLGXByrmHadot8zY7UEAIbjTK5wVvxk29D9NUKQYXfLTw10N844tnl__uQZUhDdj9qgGvAQWre_vPa3wp0v1ATgeLhQLg43q5WAv-c_eE_mo0qjPDbyN-xzs8CTlC9xJA-Cy3z2_7VnaZ_t9-GFkalLjLWHJLHldzu_m7ZyXvHR7wwn2naSKlfuCxVpoAyj2MVFu1bAnqguBHnrYQoCdIIzPtpSJ5bzTVnDEOIPJvA9HyRQQp6CIiQDukJ5WNtDUFZAuiKSxzShymbshLpBqQWcgw690NmeuiySLAirKw5Nbkz6P5THZ9hFPYJYLhN1W_z03Nuogcv3Vgw7qgrn3wTs65rLLz4_di5ZURawcLhcqSCfZfiqRQcmSUyIVj_OzfmxErIG-xMM3OUqRywb9KOmHNwkXSV9uWtWNkhbGVQ0WDA69QFLZBWrGtCB9OtHZXlhZH8nvJw9aUn5h21ue2wd3m5L79PZq6r2j_O4UDmtS8JqDXuC29rn9FGR6MjKvg5gDnAp4tOfq5Lz0TuMJGmGXvd2UAMft2E7c9168mqaM_fVZQsFJv-JDqYn0dObfa3MBy55OakPtpyC2Vp3qsznzEvRM6fHl_9nzZC4y5baH_cqp-xViGO_ey2b8BzS6XMWh6vDPWGJHVNMSsbVnGmtnqbpTDamoCn1bBhQl25xB0Ym4B_GaWOwQCvKWrujMQKFy1_zA2lrg10b1zJ3jtFyu1w2Gr9n1kTGCGjEvZplNWFIBDstuy1mn_tNgZ-MXm_hRUMpk_3jWu8sZpCSquNGMGJklK3E4G3G2jm8PrBc9nXHGkmRDra3irZp7bv8BjsFPLFo1txOrV4u8RUoPizq097MNoroR2n012i1_FqHfO5y5hu8XMv1ho6oXzPrKUEMEqEa5fC0WONEiD-6mDvVY9MxtMYu4FNPmvsc8jyUT52NnG288NzCxOeHO9bxUft7XJoa2bLMvOZv1qLufqUYK1Jbq802HqUIfaj0FBdaALXH68gUYPCfm7WWCqDKuT8DrSCuma6p2G7nwlNqGhmyGKu-O01B9xC8kJG7H3ja02JWX-dnYzg8fajTHT6K9STKXosRKG0hJbZ9LMVQDiDV84oRIoDngRMAhaW_imUq7t7_o16NhAloOD7659CGdx42Zf1HDmqCNzVH-SCd72JegSWjxMllTA0IXJaJT6_Go7VKrh8NYLj9BmLf6gH7TmnlTlmWHJHKcys-4bDoQh1oIQ9LizT9jYiDDdvQHPZ1WoiA26FpWrCoSCujco0eDlA8vdQBSlNgTyp_L4eXryIqoK1GHs4qB-w_qFFZYXNXt5N7r4FwU_vzIiF3Z4WCOhaKQjqhUGT_Nb3pZRbP7hh_IHpRDCsuQi1HxJMDq9UtCOFMwtkCzyR9ZQYnUqRP5sGCt9Zdky86xP1VrnjnlHX4m5zxi8QmkhND6y-L3trZNHfXsi1edFvPb9vEM-4D9nx81N_zQd8djmnp3BeulVwsRhCaiyg1jfr4g11495PgMnbUkZmTaFgRIWvlOcj9PG1yA9NyYdmHC-WPmCG_KmEEIJNdaR65twZkdlFzqF9-Tr6qsg_av6AGctIIFJ36a3fMLg6cQPTgX14CON6ufMXvjMT5OACHSbwKw3M5iWPj4ESlA_mHHUVCjHJkE2Pv28GyRSsnfLiVn6gWkylf5wXbK3-TKsFSgFaRjZYDzrJfJdxUaAsx6ahZYS8cLDaBB4dYAfctU5VfaQm9_wJ11ljFrcTgPP2XChFL3y7G3W-zhFMBp4sPdhOXePhg5NurWqKfm-he9lBkYAyinwY8m_NBK97emouvURJMjBcb9nzMmW9TLuCGpOvb38lRo5v9mu9OhxI3KuzFNHki2evgF2DdrqyjjPMxco_GgUlLztKZH-TWwYxChCPQ5zIgj4YpixnxelDrW2ueWsWZOSOCMCOeiitW1mYBMxXL5Ww_auG4fsHgYgbUN0jzRpvTMinlwA0q69RRGyN_u9TlnR-Zfzi9oZp7cKaqAc1tD1WC6G-PWGbhqdC64c0eKBPaG7wRQHkYcBcJxjtfXqG0dkxm4hbJMIERvWa5B1X7CEVumrIbtnWTowHF4qxr7F8nFif1ZKTaxZgGEX2RThQ0foxFHw1iEpS-Z4SjrthzK8E9CUXtFVzCR45RSuDlSH3Qoepn_dhupTIUmO5TYtvFPOodzVPYXjisq3iGrMMRW7_dYjcZaXJ8Db-VN0pkN0MhZFliVjmdXWUTJ3REPzooPUgB7nXLcVFjBV4Rw68qvNIy5S372W5D79MpTLqMgpdlSEzEiB7LBFVttrKye5h0SexjO1IzsQkEQjH2bL9muE36CthSBzuXhyM3RwEG-4DEBpG6ieXkhl6GtBRmgZictl3jaVt8qXFtbk_WHF8hx00dpcZ9lavpIiZeSSlNYWonpvc6NC1-bP1x5tY4fZIctojCZ0iU0kuMuCQ-qBG5vUhFyakQBTonEuCOXjdybpSTPdS5C6p2uvwiCmA4zFUYngxxOue5R6XUbDIoqbZECFMN31qTXBqoSOiOV7AHzdYpNh42OxJizYotOFNjBwOPtFECSJ785eBdvbmh3uw2QsYBHO8ypP2hqcoMepELxl0xtTJv2J1bIF0lE5km7MeliH_5n2AvUlPbSaaxVxS1SAhpWBrRpLnK3RRRiy3tMvGHngyp9UdhsJAKKhKQckdWEHATaen5BasI9XW1xE5Ijn1bk7eX4IKjkkp6Rtx3zVVyKXkaotu0sps2jq3iqpt6mtoDgtlrU4Ij1cWaYlVSLK-XxSYY2ivFQQbVbsPqZQtHRXpEqJW2Ur_czaJ3lDwkSadwD-SaTJNp8m0aGKEUCZi7hBbubdpAOXv1yOAh5M2j1OKg9c-3I7WoH5lcyAA23eq7TTdLqcjtfG4UymX6WKyhLjwdqhSGg12MZXaInSbLI8D3gn17jfvPRAegtewUDxrRCEIEMTY8OnV7RMjOuN5ciip20QayD6MDOspUm1BN2m4p7rfbkhweiPouZ1aBUtRniT0UChMfVdpbyx8TKk8EQwH-xXv1RUUP5g4s0cF_FfQGnTzNqcec9Osd9F8oqVegZzS3ooC3A2lv2jS6B_w1p0KSZ_0XRHltxphQslPsEDqTwNGxZNFunaja572h1PPXBHNvwYoH-wOOkZa6TsrRGPxl4fGMU9GRplQNy4UPwNwoJJtkhf8QoUexasQ2rxGYapl51VjZI3VeH5NinV1w1U5pBAxWMSFhmevNXl5hiDeIkC62wYftV-tS83acISfK0OPb3IYEtbj8AbkTRQD_95-wt4_6z_NPnKhvNWkVG4-snF7wkgVWFhXm8hOyGIKimIfTB7GqRvuTuNlDM4lYM5FYtk-VbSfNEj1PGMAK4FpIZRf6asQRBizQlpCNHzZ3Kbsi6vzQVHLx3AASTCzq3ce6ZZjXodW3okBVMLQnw0u6wjq5ms-mr_zIYK5wQkuSl_-_BJJeaNlQ6n3ngE7oPe87Drp7g0RsF1HzfZa3Pn0bUaDaYveolYcpg9PKx-ntnrx4vT3q41lHfmHgSrJIXEIF24G5dTEozOHKry3J-xBiMRYuEW4Au1XflkqYBuN93hV94Nq0H3vY0M_TXbcYF7WV9pbyAeEp5SVBq5cT156UHA-Lcaw5sj286L0EdccRRiOxDLSKPbgbN3ykF1rtQylskeOpnQcUDSIfNAGPnxLleUhPM2ilCIhg88Rp-kH-O0GFX0Ao1pF7AXk81HWldVch1Hz1RQtepuRQR3hUzji43qFCnfb_YKAi408khwHMq0av2AZ7_fL66XCYerZcRfrtR2UCtQZGhBVzOOpramrDiUUiO_EHRmm-PFjuc1x7GcxRfcpFGp66yaUbOpUXZLGabKkA6hWdCGvKRm0=","summary":[{"type":"summary_text","text":"**Analyzing dispatch loop reconnection race**"},{"type":"summary_text","text":"**Investigating dispatcher thread termination bug**"},{"type":"summary_text","text":"**Tracing new dispatcher stream subscription flow**"}]},{"id":"rs_07547ad47f060e5d016a58ab68f6f8819a83c2d92e70551553","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKt0Ei4qsx-4ZLdIBj35Nh8sNBwP5FCTMzAaxI_CKDexv3qeVJkNgMt8IYcpBTTKQDMriBFWy005R_CRd_dDtGsFeEEFD_J2GtM6icFCm6ZcszsVfQQX1s8ziI9UsNyqmQ5S86p0hpMXEKY14qCSE8bEGdGd6XaeMyHxIQ-Ap9q3JBcQZluia8gu2Asu3JTegd71PRDQQyjZJ1ohKeneCCJ_7sHZLxnnnHgaNCK2OXB2Nv5hItPO8Xwa7_fmuIiZiawCXe1QDQKneLhbrpFqiGtkwbwf9m0NwwyLfzBq-rIuSOSxVHSYCylnydGWAPkFbCW0XD3EZermwvMkTxHGtfHpQNn7lmq7siEirpgQfvwd1LQVJFzJsEFzJrvtYdp-U1cJwtNeGMYN_e48MBKRr5jLwG5BlteMnhPWlo0Kv7CY3AI-yKLiol8B5LZjNUFsVW-aHeOXAA0mNymivc2iGSZPDm-hMrD6UlA4qGZRo2y9zMYoKneVG6L3Y3o_Ho17JdwUVg_9S06dtyLn513x4Ozak8wUSQIoOtotv5aSXDV3hOH4GoVlbacTHm04BR3jtgwy_J1ItIIjodOswnONlngajUsOO1sv0Z6qqp4a7IVkl7VpqmK2c8LGwhzEoN1_2vNacm7NSpaU_dDIcOW8Vgb_GiRwuP0_a6ok7ZbLEyFIDiMk-TIEp-bcYcaUAQCdVE1l2YcJBMhxTa6foj_q0bh_rZ6uawyAjFPoeFphCYzlWP54gFs63jKi9tD-mCpLrd_Q1BnMo8BXaQ7420llgoEj4cGNU_8ItCV8eWUS0KfWp_rlmcx0DfJijUI4M1yTfeIYO4ZTwZUVNdz0_fNWEu6KrlAkxmL1HZ6_qoy6_BBUfPLy3cQEprdj6-HMTjZXWlkVgX6HI3E1YNP6EgsgdH34yfg1RVZtMDqEHe08_4inN_ZvppAlSr1PoPPP9KZlxN770GTxnD7rS7S3KVH7bYnc7upK32dD2hd7_iQqjpxr9lfriz-Xj1DGMrY6AbJOAa4FK2ESI36hcL7rz7MxvqLjvyj-FsaRkQqJVmXnxKQ8xuYiHw6O15d7Q7wr2p397Eao_LU8H-r0o9iBldT9-6ZABV9GIIs6fNfuZS0c8fLmCTnqZL94a3deAIS9_Yp94xjbhsZle8vg84uPpXJJJgI68MchnM8OPTcIwniGb1UgC1qnII58dsNtgXqNhSGOHjXdJnGGMIGY6zEPU3zynTOKYLjIe6ds9RBTtu0Z1bqrA1xQV1FURPlrGvOc38FXRtCswy3WsCQ2J1Aq1NB_tHkq33eJTQZ0k6WKme24lPKMa75J0IcPEwIGsCSKJk9mzb1opzsgSNCmyS3F8FU2BRc1_RAgsk8Inh1aywt1ZGYr2NCZzjMCrVM43njbeLKS6q9aZPc2A_nD757riJAoVdXwJRuNtTPOqS2WW74c1vvgnnZ07374-rZZkDSdwMvfl5Zmbc5wKid6hCluDvE9Gpv_jja20ZxCu7f6CbDrynPHc-es5X3tj3psdGoAXRAhQ5TynxufetYIrcpjT73IPbWobjA4CHp6hlYnf8eRb0OjWsD8o_eAWdzQ1QGBMATNNIsuzuzuReycRA-RAQdA3BOc8jvL7K4fk47hmUUSlp6tp7Ef_vUiEvgCb6zMeIMXOzpHz0yphCbppz9yFn7LlrOvsYn06OISJJUmlweau2VJNy0hatvIfdaYbPaRIg0d2A-8LSsv2sRwmOckZJwTdxuboyXgbJ3pghQPAr8OSBp7QrfZbqpS5_ho8JL3CCO8wFf8VzUh4wam4O7Xb1Dlqqf9Z35R-BR6ixMqcFAqq7XX48GhpNWZcya_OEc1C-RUuZGLxTvWGM72B0OBufKpDNZOvzm47xEGO70WrQP8GJohU3GsXf2jlZOovGdiPvZ4q4_Qxpt2pV-5SVYiVU1ydzEvHp2yCLyi1yGIHZxUh4b-0x0uYoV3PK8jLhXAgsfz9lGkD933yVvadjE_V_cA62Ff-U9yNyi0he0P5BX7FPHVslrI6BUqK4G_b8KyZV4vG8mTTM8_mnO5QZybY2imrIH-xrTFJG-QmTiBrgZcGZB4SdCYPnLAcAoLZNADkyVR_52TyqPpTeEVQ6S7Aw3sCnLxHmr6XtGq_O48DBUa6X-cpe23b0FV_V9T8e1MLynOWJkTEa34e45lcLfg9TZ_cX2pwUtZWppz_a9BUcvEcxOZa1ZpUkY1IjyRA2Kha8eOrPKQgZrXSSmkXqU41Kprbti21RifWeY_0mEa7xjzUktMrBBeHcK2WK3cxR8XlBvfdYyFlOSWfjFVVleMRBREv_IYlSGvhAI4bcDesX2elff8zjHwA0wAW4F77ekNQ-RY4jFOKAIM_nSOf3j8r9mbNu_9iKZBHoMUQOHz6zdKJAgru2nCDKq-0cQ0XNiFlF32z-CwCGOX3v5DPppJ6PQRecgCcyVngbI9tLa3jccUmNoDZmYRmwywUvjA9vB-YocB6PAMv3yn_nrP1pGDMPiurn6Czgu_0wJXui5RDZWcTKBhER-X9d-kWaBC-QO9FUuYEvRy3HX5Uta7kufIVQyeOvkGekIhlNOKTTy6zswqjm1FMERLkbJgN1QqVMjV7U7AbAx2xnM-pPhJjW0dHK8ufxjX-lWspyjtzgkSIqbew7w1tiB4Kc07NCFqX98LPtEu5pJPj3Aubf_TQiYrR1ij-wAJHfTdHSlGifikD74l_cT19zCbuZDkBHyuigUnzDqzTiyoTIU5fh-OEtdtc2zP6Kl6GwWxCBpPA1RL239AdtIty1i1R-bEtLaRbE5SPgtqgf4ppVDg6sqRm76jJb_AnH71kYoPXDSKNNX8RXljw0qMIop8HePpy3YfLFjuR1NkZxVRC_08fPghGe6EPqXF-aurHAChPMPvIFmacPMX0TJJbTPjmtrxnJiQXtyGnSqz0ZAQu3b1gsRzTers7MP-2gE7Sqeo6MtZFBeYrcdrIMdpDbfd_5wTdNwLaFRp1f68PR1j","summary":[{"type":"summary_text","text":"**Inspecting handle_conn cleanup and writer join**"},{"type":"summary_text","text":"**Analyzing network stream and controller counts**"}]},{"id":"fc_07547ad47f060e5d016a58ab74197c819abb2dd3f9eae156e5","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding four of five incident logs\",\"pattern\":\"4/5|4 of 5|four of five|BRAIN_RESUMED|detach.*reattach|detach.*rc|rc.*detach|most.*session\",\"path\":\"docs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_EE0RL5mNw6JaOG9rqkTLZS3C","name":"grep"},{"id":"fc_07547ad47f060e5d016a58ab741990819ab32920122a383b57","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain startup before loop\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"170-320\"}","call_id":"call_T6b3tTpBOa1BW1FmzeHUZ8pp","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab74199c819ab1a520ea17ca4797","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach serve subscription loop\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"230-430\"}","call_id":"call_BraTxpgPGpafq9Fm1quCaRY9","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab7419a4819ab458dd1b59bcc195","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker connection cleanup\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3020-3225,3430-3515\"}","call_id":"call_9mEKtcZqGfub6Y7u6HrkDuPa","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab7419b0819ab2fcd8738a667624","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream persistence table\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"1380-1430\"}","call_id":"call_IFwWGGC3Y19es8MPlIMSbHGO","name":"read"}]},"duration":29899.542500000447,"ttft":3888.5607000002638,"contextSnapshot":{"promptTokens":82310,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_EE0RL5mNw6JaOG9rqkTLZS3C|fc_07547ad47f060e5d016a58ab74197c819abb2dd3f9eae156e5","toolName":"grep","startedAt":"2026-07-16T09:59:18.082Z","args":{"path":"docs"},"intent":"Finding four of five incident logs"},"id":"b3b456b9","parentId":"df4d1fd9","timestamp":"2026-07-16T09:59:18.082Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_T6b3tTpBOa1BW1FmzeHUZ8pp|fc_07547ad47f060e5d016a58ab741990819ab32920122a383b57","toolName":"read","startedAt":"2026-07-16T09:59:18.082Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain startup before loop"},"id":"0b98d01a","parentId":"b3b456b9","timestamp":"2026-07-16T09:59:18.082Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BraTxpgPGpafq9Fm1quCaRY9|fc_07547ad47f060e5d016a58ab74199c819ab1a520ea17ca4797","toolName":"read","startedAt":"2026-07-16T09:59:18.082Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach serve subscription loop"},"id":"28f0ed9c","parentId":"0b98d01a","timestamp":"2026-07-16T09:59:18.082Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9mEKtcZqGfub6Y7u6HrkDuPa|fc_07547ad47f060e5d016a58ab7419a4819ab458dd1b59bcc195","toolName":"read","startedAt":"2026-07-16T09:59:18.082Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker connection cleanup"},"id":"4925d4cc","parentId":"28f0ed9c","timestamp":"2026-07-16T09:59:18.082Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IFwWGGC3Y19es8MPlIMSbHGO|fc_07547ad47f060e5d016a58ab7419b0819ab2fcd8738a667624","toolName":"read","startedAt":"2026-07-16T09:59:18.082Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream persistence table"},"id":"726f02e3","parentId":"4925d4cc","timestamp":"2026-07-16T09:59:18.082Z"}
{"type":"message","id":"f3815c6a","parentId":"726f02e3","timestamp":"2026-07-16T09:59:18.136Z","message":{"role":"toolResult","toolCallId":"call_9mEKtcZqGfub6Y7u6HrkDuPa|fc_07547ad47f060e5d016a58ab7419a4819ab458dd1b59bcc195","toolName":"read","content":[{"type":"text","text":"    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        loop {\n…\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n…\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    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\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    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: InputReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\n…\n    }"}],"details":{"displayContent":{"text":"    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        loop {\n…\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n…\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    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\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    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: InputReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\n…\n    }","startLine":2980,"lineNumbers":[2980,null,3010,null,3016,3017,null,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,null,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,3514,3515,3516,null,3598]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784195958136}}
{"type":"message","id":"db4d54cb","parentId":"f3815c6a","timestamp":"2026-07-16T09:59:18.139Z","message":{"role":"toolResult","toolCallId":"call_EE0RL5mNw6JaOG9rqkTLZS3C|fc_07547ad47f060e5d016a58ab74197c819abb2dd3f9eae156e5","toolName":"grep","content":[{"type":"text","text":"# docs/\n## DEFERRED.md\n 45|| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46|| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47|| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48|| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n 49|| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n## KNOWN-HAZARDS.md\n 215|- **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 ...\n*216|- **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`...\n 217|- **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 218|- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n 219|\n...\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*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*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 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...\n 449|- **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*450|- **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 Wi...\n 451|- **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; t...\n 452|- **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 453|- **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 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...\n 471|- **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*472|- **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, ...\n*473|- **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*474|- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).\n 475|\n 476|<!-- [doc->REQ-RC-WIN-PASTE] -->\n 477|### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\n...\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 561|- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).\n...\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 580|- **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....\n...\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 628|- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 629|- **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 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...\n 642|- **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_drain...\n 643|- **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 708|| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n*709|| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\n 710|| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\n 711|| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.r...\n*712|| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |\n 713|| 7.17 | PTY **input** is single-writer: each session spawns ONE dedicated input-writer thread = the SOLE caller of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`). Every caller (`dispatch_input`, `dispatch_endpoint_input`, the inject worker/floor flush) ENQUEUES (`try_send`) + returns at once — a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. Full queue ⇒ DROP excess + stamp the perch `input_backpressure` (heal-on-resume: ...\n 714|| 7.18 | `spt rc` paste is **client-originated** on Windows: CC runs daemon-side with no access to the operator's LOCAL clipboard. On a RIGHT-CLICK rc reads the local clipboard itself and injects a BRACKETED paste (`ESC[200~` + content + `ESC[201~`) — CC has bracketed-paste mode on (`ESC[?2004h`), so a multi-line paste lands intact with NO `\\r` submit-storm, harness-agnostic, content VERBATIM. RawGuard captures the mouse (disables console QuickEdit so right-click reaches the app) on an interactive cons...\n 715|| 7.19 | An operator input FLOOD must not deadlock the broker: `serve_attach` sends N `Input` frames on one conn without reading, the broker acks each on the SAME conn → the return direction fills (~pipe buffer) → `send_frame` blocks the per-conn handler → mutual full-duplex DEADLOCK (permanent broker wedge, controller latched). FIX: opt-in ack — `InputReq.ack` (serde default true); the operator/rc path sends `ack=false` (`send_effect_no_ack`), `dispatch_input` skips the applied frame → the han...\n...\n 717|| 7.21 | Exactly ONE LIVE `controller_writer` per brain↔broker connection + every writer emits an ASCENDING seq stream ⇒ a snap-above consumer over the surviving writer's COMPLETE `[0,end]` replay delivers `[K,end]` with no skip/dup. A brain-restart re-serve double-registered the controller (handoff's `subscribe(from_seq=K)` + the re-serve's `attach_as(sid,0)`) → two writers raced one socket → the consumer's strict reject-gap legacy path saw `got seq 1 want 0` (flaky `attach_survives_target_brain...\n*718|| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-...\n 719|<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->\n 720|| 7.23 | An already-spooled message (WAN-arrived or spooled-while-active) NEVER depends on adapter hook-poll cadence to reach a relay-less spt-hosted endpoint — the daemon drives delivery on the events it owns. TWO daemon triggers feed ONE shared translation-binary inject leg (`spt_daemon::inject::try_spt_hosted_inject`): WAN ingress (`receive_wan` injects before spool — leg 1, `[REQ-WAN-SPT-HOSTED-DELIVERY]`) + the ACTIVE→IDLE edge (`drain_idle_window` claims the pending spool — deferred AND non...\n 721|<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n## MANIFEST.md\n 527|\n*528|**Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** ...\n 529|\n 530|Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for ...\n 531|\n## NEXT-MILESTONE-BUG-TRIAGE.md\n 111|\n*112|**C-#5 endpoint-list local node name** · conf H · effort S. ROOT: `render_local_section` (cli.rs:4359) hardcodes `\"LOCAL (this node)\\n\"` via print_local_section(4349)←cmd_endpoint_list(2516). Node-ident render already at cli.rs:5531-5542 (os_hostname + nodeid::load_or_create().public_key().to_hex() → `label (prefix…)`), open-coded ~6 places (2901/3006/3124/5641/6019/5930), no helper. (SELF pin = perch id, NOT node id — don't reuse.) FIX: factor `node_ident_display()`, compute in impure print_lo...\n 113|\n 114|**C-#11 endpoint-list palette parity** · conf H · effort M · COUPLED #13. ROOT: `format_subnet_rows` (cli.rs:2529) prints raw `{:?}` status (2538), no color/glyph; picker derives from SAME ResourceRow: Status→EpStatus (data.rs:273-278), bound/controller/harness (289-323), display_status()→EpDisplay (model.rs:292), square_span (view.rs:64)+display_color (view.rs:28). Shared input = ResourceRow (registry.rs:383). BLOCKER: picker submods private (picker/mod.rs:18-21). FIX: (1) pub EpStatus/EpDisplay/...\n 115|\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n 55|handled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within\n*56|seconds: attached rc output freezes; detach does NOT release the control stamp (release routes\n 57|through the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays\n 58|healthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled\n 59|or slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\n...\n 99|**REQ-DAEMON-STDERR-PERSIST** (impl, unit)\n*100|Observability: detached daemon nulls stdio → the incident window left ZERO logs (both RCAs ran\n 101|blind; rigs had to recreate everything). Fix: broker + brain stderr tee to a rotating file\n 102|under SPT_HOME (size-capped, e.g. 2×5MB), stamped per generation. KNOWN-HAZARDS note: never\n 103|inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.\n## NEXT-MILESTONE-RUNTRUTH-TRIAGE.md\n 201|`si1-probe` on HFENDULEAM). GREEN halves: endpoint-info VALIDATED, SI-1\n*202|relative drop dirs VALIDATED E2E, CONTROLLED clear-on-rc-detach VALIDATED on a\n 203|healthy daemon. Remote-row truth ENV-BLOCKED (no live remote in window).\n 204|\n*205|### B5 (perri F-a). Peer pump stall under rc attach/detach churn\n 206|[DEFECT, daemon, OBSERVED-ONCE, HIGH]. Fresh 0.22.0 daemon, ~10 min after\n*207|restart, during rapid rc attach/EOF-detach/--take cycling: `peer pump: STALLED\n 208|(last tick 122s)`; while stalled `spt rc --view` → `RC_FAIL: attach request:\n 209|brain IPC read deadline elapsed` (repeatable) and controlled-clear stopped\n 210|propagating. Daemon restart recovered; endpoints auto-revived. Priors in this\n...\n 216|02:13–02:16, all stdin-EOF detaches (never ctrl-b d):\n*217|  (1) `printf '\\r' | timeout 6 spt rc <id>` control-attach EOF-detach;\n 218|  (2) `timeout 8 spt rc <id> --view` ×2;\n 219|  (3) re-attach → REFUSED \"take control (kicks <node>)\" — the controller\n 220|      record from (1) STILL HELD after its EOF-detach (pre-stall detach-clear\n## W2-DESIGN-PROPOSAL.md\n 14|   stalled brain self-heals by rewind).\n*15|2. Control verbs (take / release / detach-stamp / reattach-refuse) resolve against\n 16|   BROKER-owned truth — the `OutputLog` controller table + perch stamps; the broker\n 17|   is already the single writer of `driven_by`/`viewer_count`\n 18|   (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) — with NO brain round-trip.\n## W5-GATE-HOLD.md\n 11|\n*12|**Flaky ~17% on Windows, ISOLATED (single test, no parallel load): 2/12 fail, 4/5 fail, etc.** This is NOT a load-class flake (not isolated-green like the W2 ledger entries) — it is an inherent timing race, so it cannot be flake-ledgered. It is on the seam W5-A changed.\n 13|\n 14|**Root (diagnosed):** `c1_miss` is a **pre-existing** test (not W5-added). W5-A's **settle-gate adds a 400ms `INJECT_SETTLE_TIMEOUT` per inject** on this test's mock PTY (the fixture never answers the DSR probe, so the bounded wait always elapses — visible in the log every run). The test observes with **FIXED sleeps** tuned pre-settle-gate (`inject_control_wedge.rs`):\n 15|- L1867 `sleep(800ms)` after event 1 (to let the 400ms commit-deadline elapse),\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 28|\n*29|**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30|\n 31|## Consequences\n 32|\n...\n 36|- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n*37|- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n 38|- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n 39|\n 40|## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n### 0007-subnet-notification-primitive.md\n 8|\n*9|Three flows independently needed \"deliver something to the user's most-recently-active session and make sure they see it\": the **self-update prompt** (CONTEXT §Self-update), the **consent escalation** (CONTEXT §Consent & security gates — both already described as \"the registry-resolution precursor to PresenceChannel\"), and emerging **subnet events** (node paired, external-subnet pairing request). Each was about to grow its own ad-hoc delivery + \"did the user see it?\" handling. They are the same thing.\n 10|\n 11|## Decision\n 12|\n...\n 16|- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n*17|- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18|- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19|- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n 20|\n### 0025-live-daemon-coordinated-adapter-update.md\n 52|\n*53|**Correction 3 / the third axis — *ephemeral-but-install-dir-locking → run from a per-endpoint own copy*.** The resident/ephemeral split needs a third class: an **ephemeral** binary (excluded from the daemon-coordinated stop/restart, self-heals on next spawn) that nonetheless **locks the install dir while it runs** (a detached, unsupervised, long-lived install-dir binary). The fix is to run it from a **per-endpoint private copy** under `<perch>/.live-bin/<program>` (copied on first spawn / when the h...\n 54|\n 55|**Net shipped flow.** CLI fetches + verifies + extracts; for an adapter with no live endpoint it CRC-swaps directly; with a live endpoint it hands the apply to the broker, which per affected endpoint **stops the resident translation child** (releasing the OS lock) → **CRC-swaps only changed files** (never-strand) → **restarts the translation from the NEW on-disk manifest path**. The Psyche (own copy) is untouched and picks up new bytes on its next respawn; the brain self-heals its manifest via reload...\n 56|\n...\n 70|\n*71|The **Amendment (W3 build)** above (Corrections 1 & 3) built its resident/ephemeral analysis on a premise that F-030 (psyche-ephemeral) now **retires**: that the Psyche is a **long-lived process** — Correction 1's \"separate, detached, unsupervised install-dir binary,\" Correction 3's per-endpoint **`.live-bin` own copy** run \"from a per-endpoint private copy … torn down with the perch.\" Both are superseded:\n 72|\n 73|- **The Psyche is now a bounded PER-EVENT TURN, not a resident process.** `[session.psyche_init]` is a **go-live gate only** (its presence promotes the endpoint to a LiveAgent; spt-core never spawns it); each psyche-relevant event (pulse fire, commune/signoff drop, custody transition) runs exactly **one** bounded turn through **`[session.psyche_resume]` — the sole driven psyche role** — spawned by the daemon, exits at turn end. **Liveness = turns succeed** — there is no resident Psyche pid to super...\n 74|- **The `.live-bin` per-endpoint own copy (Correction 3) is dissolved.** The Psyche spawns from the registry install dir like every other bounded adapter invocation (REQ-INSTALL-11 unchanged — a manifest that still names an own-copy path resolves install-dir-first anyway). Any existing `owlery/*/.live-bin/` dirs are swept (displace+GC if image-locked at update time). The **resident translation binary remains the only install-dir locker** the daemon-coordinated apply stops/restarts — that half of Corr...\n\n## design/\n### v0.16.0-update-arc-and-cli.md\n 141|\n*142|→ `REQ-DIGEST-CURSOR` (extends REQ-TERM-4/5). Docs: reference.md, gh-pages digest/integration.\n 143|\n 144|### B3. Global `--json` for status queries + stable output DTOs\n 145|\n\n[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":35,"fileCount":12,"files":["docs/DEFERRED.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md","docs/W2-DESIGN-PROPOSAL.md","docs/W5-GATE-HOLD.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0007-subnet-notification-primitive.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md","docs/design/v0.16.0-update-arc-and-cli.md"],"fileMatches":[{"path":"docs/DEFERRED.md","count":1},{"path":"docs/KNOWN-HAZARDS.md","count":17},{"path":"docs/MANIFEST.md","count":1},{"path":"docs/NEXT-MILESTONE-BUG-TRIAGE.md","count":1},{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","count":2},{"path":"docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md","count":4},{"path":"docs/W2-DESIGN-PROPOSAL.md","count":1},{"path":"docs/W5-GATE-HOLD.md","count":1},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":2},{"path":"docs/adr/0007-subnet-notification-primitive.md","count":2},{"path":"docs/adr/0025-live-daemon-coordinated-adapter-update.md","count":2},{"path":"docs/design/v0.16.0-update-arc-and-cli.md","count":1}],"truncated":true,"displayContent":"# docs/\n## DEFERRED.md\n 45│| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46│| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47│| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48│| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n 49│| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n## KNOWN-HAZARDS.md\n 215│- **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 ...\n*216│- **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`...\n 217│- **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 218│- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n 219│\n    │...\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*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*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 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    │...\n 449│- **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*450│- **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 Wi...\n 451│- **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; t...\n 452│- **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 453│- **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 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    │...\n 471│- **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*472│- **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, ...\n*473│- **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*474│- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).\n 475│\n 476│<!-- [doc->REQ-RC-WIN-PASTE] -->\n 477│### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\n    │...\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 561│- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).\n    │...\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 580│- **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....\n    │...\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 628│- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 629│- **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 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...\n 642│- **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_drain...\n 643│- **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 708│| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n*709│| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\n 710│| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |\n 711│| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.r...\n*712│| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |\n 713│| 7.17 | PTY **input** is single-writer: each session spawns ONE dedicated input-writer thread = the SOLE caller of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`). Every caller (`dispatch_input`, `dispatch_endpoint_input`, the inject worker/floor flush) ENQUEUES (`try_send`) + returns at once — a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. Full queue ⇒ DROP excess + stamp the perch `input_backpressure` (heal-on-resume: ...\n 714│| 7.18 | `spt rc` paste is **client-originated** on Windows: CC runs daemon-side with no access to the operator's LOCAL clipboard. On a RIGHT-CLICK rc reads the local clipboard itself and injects a BRACKETED paste (`ESC[200~` + content + `ESC[201~`) — CC has bracketed-paste mode on (`ESC[?2004h`), so a multi-line paste lands intact with NO `\\r` submit-storm, harness-agnostic, content VERBATIM. RawGuard captures the mouse (disables console QuickEdit so right-click reaches the app) on an interactive cons...\n 715│| 7.19 | An operator input FLOOD must not deadlock the broker: `serve_attach` sends N `Input` frames on one conn without reading, the broker acks each on the SAME conn → the return direction fills (~pipe buffer) → `send_frame` blocks the per-conn handler → mutual full-duplex DEADLOCK (permanent broker wedge, controller latched). FIX: opt-in ack — `InputReq.ack` (serde default true); the operator/rc path sends `ack=false` (`send_effect_no_ack`), `dispatch_input` skips the applied frame → the han...\n    │...\n 717│| 7.21 | Exactly ONE LIVE `controller_writer` per brain↔broker connection + every writer emits an ASCENDING seq stream ⇒ a snap-above consumer over the surviving writer's COMPLETE `[0,end]` replay delivers `[K,end]` with no skip/dup. A brain-restart re-serve double-registered the controller (handoff's `subscribe(from_seq=K)` + the re-serve's `attach_as(sid,0)`) → two writers raced one socket → the consumer's strict reject-gap legacy path saw `got seq 1 want 0` (flaky `attach_survives_target_brain...\n*718│| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-...\n 719│<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->\n 720│| 7.23 | An already-spooled message (WAN-arrived or spooled-while-active) NEVER depends on adapter hook-poll cadence to reach a relay-less spt-hosted endpoint — the daemon drives delivery on the events it owns. TWO daemon triggers feed ONE shared translation-binary inject leg (`spt_daemon::inject::try_spt_hosted_inject`): WAN ingress (`receive_wan` injects before spool — leg 1, `[REQ-WAN-SPT-HOSTED-DELIVERY]`) + the ACTIVE→IDLE edge (`drain_idle_window` claims the pending spool — deferred AND non...\n 721│<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n## MANIFEST.md\n 527│\n*528│**Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** ...\n 529│\n 530│Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for ...\n 531│\n## NEXT-MILESTONE-BUG-TRIAGE.md\n 111│\n*112│**C-#5 endpoint-list local node name** · conf H · effort S. ROOT: `render_local_section` (cli.rs:4359) hardcodes `\"LOCAL (this node)\\n\"` via print_local_section(4349)←cmd_endpoint_list(2516). Node-ident render already at cli.rs:5531-5542 (os_hostname + nodeid::load_or_create().public_key().to_hex() → `label (prefix…)`), open-coded ~6 places (2901/3006/3124/5641/6019/5930), no helper. (SELF pin = perch id, NOT node id — don't reuse.) FIX: factor `node_ident_display()`, compute in impure print_lo...\n 113│\n 114│**C-#11 endpoint-list palette parity** · conf H · effort M · COUPLED #13. ROOT: `format_subnet_rows` (cli.rs:2529) prints raw `{:?}` status (2538), no color/glyph; picker derives from SAME ResourceRow: Status→EpStatus (data.rs:273-278), bound/controller/harness (289-323), display_status()→EpDisplay (model.rs:292), square_span (view.rs:64)+display_color (view.rs:28). Shared input = ResourceRow (registry.rs:383). BLOCKER: picker submods private (picker/mod.rs:18-21). FIX: (1) pub EpStatus/EpDisplay/...\n 115│\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n  55│handled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within\n *56│seconds: attached rc output freezes; detach does NOT release the control stamp (release routes\n  57│through the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays\n  58│healthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled\n  59│or slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent\n    │...\n  99│**REQ-DAEMON-STDERR-PERSIST** (impl, unit)\n*100│Observability: detached daemon nulls stdio → the incident window left ZERO logs (both RCAs ran\n 101│blind; rigs had to recreate everything). Fix: broker + brain stderr tee to a rotating file\n 102│under SPT_HOME (size-capped, e.g. 2×5MB), stamped per generation. KNOWN-HAZARDS note: never\n 103│inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.\n## NEXT-MILESTONE-RUNTRUTH-TRIAGE.md\n 201│`si1-probe` on HFENDULEAM). GREEN halves: endpoint-info VALIDATED, SI-1\n*202│relative drop dirs VALIDATED E2E, CONTROLLED clear-on-rc-detach VALIDATED on a\n 203│healthy daemon. Remote-row truth ENV-BLOCKED (no live remote in window).\n 204│\n*205│### B5 (perri F-a). Peer pump stall under rc attach/detach churn\n 206│[DEFECT, daemon, OBSERVED-ONCE, HIGH]. Fresh 0.22.0 daemon, ~10 min after\n*207│restart, during rapid rc attach/EOF-detach/--take cycling: `peer pump: STALLED\n 208│(last tick 122s)`; while stalled `spt rc --view` → `RC_FAIL: attach request:\n 209│brain IPC read deadline elapsed` (repeatable) and controlled-clear stopped\n 210│propagating. Daemon restart recovered; endpoints auto-revived. Priors in this\n    │...\n 216│02:13–02:16, all stdin-EOF detaches (never ctrl-b d):\n*217│  (1) `printf '\\r' | timeout 6 spt rc <id>` control-attach EOF-detach;\n 218│  (2) `timeout 8 spt rc <id> --view` ×2;\n 219│  (3) re-attach → REFUSED \"take control (kicks <node>)\" — the controller\n 220│      record from (1) STILL HELD after its EOF-detach (pre-stall detach-clear\n## W2-DESIGN-PROPOSAL.md\n 14│   stalled brain self-heals by rewind).\n*15│2. Control verbs (take / release / detach-stamp / reattach-refuse) resolve against\n 16│   BROKER-owned truth — the `OutputLog` controller table + perch stamps; the broker\n 17│   is already the single writer of `driven_by`/`viewer_count`\n 18│   (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) — with NO brain round-trip.\n## W5-GATE-HOLD.md\n 11│\n*12│**Flaky ~17% on Windows, ISOLATED (single test, no parallel load): 2/12 fail, 4/5 fail, etc.** This is NOT a load-class flake (not isolated-green like the W2 ledger entries) — it is an inherent timing race, so it cannot be flake-ledgered. It is on the seam W5-A changed.\n 13│\n 14│**Root (diagnosed):** `c1_miss` is a **pre-existing** test (not W5-added). W5-A's **settle-gate adds a 400ms `INJECT_SETTLE_TIMEOUT` per inject** on this test's mock PTY (the fixture never answers the DSR probe, so the bounded wait always elapses — visible in the log every run). The test observes with **FIXED sleeps** tuned pre-settle-gate (`inject_control_wedge.rs`):\n 15│- L1867 `sleep(800ms)` after event 1 (to let the 400ms commit-deadline elapse),\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 28│\n*29│**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30│\n 31│## Consequences\n 32│\n   │...\n 36│- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n*37│- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n 38│- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n 39│\n 40│## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n### 0007-subnet-notification-primitive.md\n  8│\n *9│Three flows independently needed \"deliver something to the user's most-recently-active session and make sure they see it\": the **self-update prompt** (CONTEXT §Self-update), the **consent escalation** (CONTEXT §Consent & security gates — both already described as \"the registry-resolution precursor to PresenceChannel\"), and emerging **subnet events** (node paired, external-subnet pairing request). Each was about to grow its own ad-hoc delivery + \"did the user see it?\" handling. They are the same thing.\n 10│\n 11│## Decision\n 12│\n   │...\n 16│- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n*17│- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18│- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19│- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n 20│\n### 0025-live-daemon-coordinated-adapter-update.md\n 52│\n*53│**Correction 3 / the third axis — *ephemeral-but-install-dir-locking → run from a per-endpoint own copy*.** The resident/ephemeral split needs a third class: an **ephemeral** binary (excluded from the daemon-coordinated stop/restart, self-heals on next spawn) that nonetheless **locks the install dir while it runs** (a detached, unsupervised, long-lived install-dir binary). The fix is to run it from a **per-endpoint private copy** under `<perch>/.live-bin/<program>` (copied on first spawn / when the h...\n 54│\n 55│**Net shipped flow.** CLI fetches + verifies + extracts; for an adapter with no live endpoint it CRC-swaps directly; with a live endpoint it hands the apply to the broker, which per affected endpoint **stops the resident translation child** (releasing the OS lock) → **CRC-swaps only changed files** (never-strand) → **restarts the translation from the NEW on-disk manifest path**. The Psyche (own copy) is untouched and picks up new bytes on its next respawn; the brain self-heals its manifest via reload...\n 56│\n   │...\n 70│\n*71│The **Amendment (W3 build)** above (Corrections 1 & 3) built its resident/ephemeral analysis on a premise that F-030 (psyche-ephemeral) now **retires**: that the Psyche is a **long-lived process** — Correction 1's \"separate, detached, unsupervised install-dir binary,\" Correction 3's per-endpoint **`.live-bin` own copy** run \"from a per-endpoint private copy … torn down with the perch.\" Both are superseded:\n 72│\n 73│- **The Psyche is now a bounded PER-EVENT TURN, not a resident process.** `[session.psyche_init]` is a **go-live gate only** (its presence promotes the endpoint to a LiveAgent; spt-core never spawns it); each psyche-relevant event (pulse fire, commune/signoff drop, custody transition) runs exactly **one** bounded turn through **`[session.psyche_resume]` — the sole driven psyche role** — spawned by the daemon, exits at turn end. **Liveness = turns succeed** — there is no resident Psyche pid to super...\n 74│- **The `.live-bin` per-endpoint own copy (Correction 3) is dissolved.** The Psyche spawns from the registry install dir like every other bounded adapter invocation (REQ-INSTALL-11 unchanged — a manifest that still names an own-copy path resolves install-dir-first anyway). Any existing `owlery/*/.live-bin/` dirs are swept (displace+GC if image-locked at update time). The **resident translation binary remains the only install-dir locker** the daemon-coordinated apply stops/restarts — that half of Corr...\n\n## design/\n### v0.16.0-update-arc-and-cli.md\n 141│\n*142│→ `REQ-DIGEST-CURSOR` (extends REQ-TERM-4/5). Docs: reference.md, gh-pages digest/integration.\n 143│\n 144│### B3. Global `--json` for status queries + stable output DTOs\n 145│","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195958139}}
{"type":"message","id":"a5246e45","parentId":"db4d54cb","timestamp":"2026-07-16T09:59:18.148Z","message":{"role":"toolResult","toolCallId":"call_BraTxpgPGpafq9Fm1quCaRY9|fc_07547ad47f060e5d016a58ab74199c819ab1a520ea17ca4797","toolName":"read","content":[{"type":"text","text":"/// (`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…\n                    }\n…\n                }\n            }\n…\n        }\n    }\n}\n\n[Showing lines 229-433 of 627. Use :434 to continue]"}],"details":{"truncation":{"content":"/// (`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","truncated":true,"truncatedBy":"lines","totalLines":399,"totalBytes":12670,"outputLines":205,"outputBytes":12670,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// (`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…\n                    }\n…\n                }\n            }\n…\n        }\n    }\n}","startLine":229,"lineNumbers":[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,null,442,null,456,457,null,540,541,542]},"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":12670,"outputLines":205,"outputBytes":12670,"shownRange":{"start":229,"end":433},"nextOffset":434}}},"isError":false,"timestamp":1784195958147}}
{"type":"message","id":"4be606b2","parentId":"a5246e45","timestamp":"2026-07-16T09:59:18.161Z","message":{"role":"toolResult","toolCallId":"call_IFwWGGC3Y19es8MPlIMSbHGO|fc_07547ad47f060e5d016a58ab7419b0819ab2fcd8738a667624","toolName":"read","content":[{"type":"text","text":"    /// side after the write (the peer's read pump then sees a clean end).\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…\n    }\n\n[Showing lines 1379-1433 of 1881. Use :1434 to continue]"}],"details":{"truncation":{"content":"    /// side after the write (the peer's read pump then sees a clean end).\r\n    // [impl->REQ-DAEMON-2]\r\n    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\r\n        let entry = {\r\n            let streams = self.shared.streams.lock().unwrap();\r\n            Arc::clone(streams.get(&stream_id).ok_or_else(|| {\r\n                io::Error::new(\r\n                    io::ErrorKind::NotFound,\r\n                    format!(\"no such stream {stream_id}\"),\r\n                )\r\n            })?)\r\n        };\r\n        let bytes = bytes.to_vec();\r\n        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a\r\n        // black-holed QUIC stream blocks on flow-control the dead peer never grants.\r\n        // Loopback (in-process) never hangs, so the bound is a no-op for it.\r\n        self.bounded_block_on(async move {\r\n            let mut send = entry.send.lock().await;\r\n            if !bytes.is_empty() {\r\n                send.write_all(&bytes).await?;\r\n            }\r\n            if finish {\r\n                send.finish().await?;\r\n            }\r\n            Ok(())\r\n        })\r\n    }\r\n\r\n    /// Every held stream's info — the query a re-attaching brain uses to find\r\n    /// streams (including peer-initiated ones that arrived while it was down).\r\n    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {\r\n        let streams = self.shared.streams.lock().unwrap();\r\n        streams\r\n            .iter()\r\n            .map(|(id, e)| {\r\n                let log = e.log.lock().unwrap();\r\n                NetStreamInfo {\r\n                    stream_id: *id,\r\n                    conn_id: e.conn_id,\r\n                    remote_id_hex: e.remote_id_hex.clone(),\r\n                    next_seq: log.next_seq,\r\n                    finished: log.finished,\r\n                    initiated_locally: e.initiated_locally,\r\n                }\r\n            })\r\n            .collect()\r\n    }\r\n\r\n    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\r\n    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\r\n    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\r\n    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\r\n    /// No-op if the stream is gone.\r\n    // [impl->REQ-SHELL-4]\r\n    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {\r","truncated":true,"truncatedBy":"lines","totalLines":503,"totalBytes":2398,"outputLines":55,"outputBytes":2398,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    /// side after the write (the peer's read pump then sees a clean end).\r\n    // [impl->REQ-DAEMON-2]\r\n    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\r\n        let entry = {\r\n            let streams = self.shared.streams.lock().unwrap();\r\n            Arc::clone(streams.get(&stream_id).ok_or_else(|| {\r\n                io::Error::new(\r\n                    io::ErrorKind::NotFound,\r\n                    format!(\"no such stream {stream_id}\"),\r\n                )\r\n            })?)\r\n        };\r\n        let bytes = bytes.to_vec();\r\n        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a\r\n        // black-holed QUIC stream blocks on flow-control the dead peer never grants.\r\n        // Loopback (in-process) never hangs, so the bound is a no-op for it.\r\n        self.bounded_block_on(async move {\r\n            let mut send = entry.send.lock().await;\r\n            if !bytes.is_empty() {\r\n                send.write_all(&bytes).await?;\r\n            }\r\n            if finish {\r\n                send.finish().await?;\r\n            }\r\n            Ok(())\r\n        })\r\n    }\r\n\r\n    /// Every held stream's info — the query a re-attaching brain uses to find\r\n    /// streams (including peer-initiated ones that arrived while it was down).\r\n    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {\r\n        let streams = self.shared.streams.lock().unwrap();\r\n        streams\r\n            .iter()\r\n            .map(|(id, e)| {\r\n                let log = e.log.lock().unwrap();\r\n                NetStreamInfo {\r\n                    stream_id: *id,\r\n                    conn_id: e.conn_id,\r\n                    remote_id_hex: e.remote_id_hex.clone(),\r\n                    next_seq: log.next_seq,\r\n                    finished: log.finished,\r\n                    initiated_locally: e.initiated_locally,\r\n                }\r\n            })\r\n            .collect()\r\n    }\r\n\r\n    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\r\n    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\r\n    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\r\n    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\r\n    /// No-op if the stream is gone.\r\n    // [impl->REQ-SHELL-4]\r\n    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {\r\n…\n    }","startLine":1379,"lineNumbers":[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,null,1437]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1881,"totalBytes":2398,"outputLines":55,"outputBytes":2398,"shownRange":{"start":1379,"end":1433},"nextOffset":1434}}},"isError":false,"timestamp":1784195958161}}
{"type":"message","id":"19528c6b","parentId":"4be606b2","timestamp":"2026-07-16T09:59:18.174Z","message":{"role":"toolResult","toolCallId":"call_T6b3tTpBOa1BW1FmzeHUZ8pp|fc_07547ad47f060e5d016a58ab741990819ab32920122a383b57","toolName":"read","content":[{"type":"text","text":"pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n…\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 169-323 of 2116. Use :324 to continue]"}],"details":{"truncation":{"content":"    // 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":1948,"totalBytes":9687,"outputLines":155,"outputBytes":9687,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n…\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":159,"lineNumbers":[159,null,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":9687,"outputLines":155,"outputBytes":9687,"shownRange":{"start":169,"end":323},"nextOffset":324}}},"isError":false,"timestamp":1784195958174}}
{"type":"message","id":"a84123dd","parentId":"19528c6b","timestamp":"2026-07-16T09:59:36.652Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing dispatcher startup race condition**\n\n**Identifying stream ring rollover causing classification loss**"},{"type":"toolCall","id":"call_GHhV4t02zs1s3bUeWrJQevCQ|fc_07547ad47f060e5d016a58ab880b44819abb721cea74ecc2ca","name":"grep","arguments":{"i":"Locating network stream ring behavior","pattern":"struct StreamLog|impl StreamLog|ring_cap|DEFAULT.*RING|NET.*RING|next_seq|subscribe\\(","path":"crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_wbTihobH3jDjIwO88208MKCD|fc_07547ad47f060e5d016a58ab880c78819a8a30002c01bb6eff","name":"grep","arguments":{"i":"Finding dispatch restart coverage gap","pattern":"run_dispatch_loop|spawn_dispatcher|classify_first_line|worker\\(|peek_first_line","path":"crates/spt-daemon/tests;crates/spt/tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_uMSu7fCb1RQukmVBwNyBiw8W|fc_07547ad47f060e5d016a58ab880d00819a93ef5a035e97dc04","name":"read","arguments":{"i":"Reading stream log implementation","path":"crates/spt-daemon/src/nethost.rs","selector":"180-340,1100-1170"}},{"type":"toolCall","id":"call_nqpr30DC1YrPIEQZGGxIHLS1|fc_07547ad47f060e5d016a58ab880db8819aaf973a2efc70d902","name":"read","arguments":{"i":"Reading attach test restart sequence","path":"crates/spt-daemon/tests/attach.rs","selector":"770-920"}},{"type":"toolCall","id":"call_r318rwf13EbOdwGpZLeMSxkD|fc_07547ad47f060e5d016a58ab880dd0819aa4f3d4159e785347","name":"read","arguments":{"i":"Reading dispatcher tests","path":"crates/spt-daemon/src/dispatch.rs","selector":"680-759"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":23880,"output":704,"cacheRead":78336,"cacheWrite":0,"totalTokens":102920,"reasoningTokens":418,"cost":{"input":0.1194,"output":0.02112,"cacheRead":0.039168,"cacheWrite":0,"total":0.17968800000000001}},"stopReason":"toolUse","timestamp":1784195958215,"responseId":"resp_07547ad47f060e5d016a58ab76a208819a94d5e372a89c2b69","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab7a0dc8819ab567d35154096fc3","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuI8Sk9-sCvMl6MAAwlJ1TUzYfmU3lP05RIDaW3biXyJjtBZ1jVNnLldeVbWqXjpEkG61B_SxF8Dtfrs1q9lL4Gtfmw5KxxtTKMYkSfgg2KNCcOHlmhMU74Cy6hrIJeENerEaWitbTmhQcDVcvTz4y4BtlNpjNzOIhqWlIc5Nki-BCQRVTD-rbgC14l34V6PbcBc383YpjateswHaTNrNOMqi_hy-EAnIBGxRoTfx1beYEK6k9jRtFGSbKWEPhJKqWGao9e6zPA7xQLOK_SM1wKxqrA9XO3WayyvHIQrZljWbEMPr4sxZBIE7fyP5zs-6gdjfgin0WayqAOWA6dtTIujY9b6lov5dMXmBJ3lrYfclsUd_rlhpUFygL5e3hDz2zLGCaUgDe6UcO5UUPbiLU-zozuNWJkWv_Eg3bDfSW3iLamvSKkGLPqN1zN45A_vI8iOMAPE7KCUMlV5_e9T6wpRDpeExMO-qO19vhSmlE3U1Ze2nPdJC69PYz2i7-Q2VUjZZLJmres107oe2-bsbaLfLevGaLwPkuhesQBuk1c8Nug4YYJVLY5mK4t_zA8GnJf0EasjCkVn5nGiINWgXVPjhGF5kdR5GgHssc5mRjD7mYHaYqwyaOu_Wpq9_u1L88JISOXd8-xSqYUQz_JqN24kzHyUphWS7OvWdCeqsyvrvsYOSAIZGhO7qa3-cLY7x1IrZpwmFCEa5ye1R7Fb4SHqjpfHDttGrGwc49Aqh8ZJvfFskEvCBCinQHDNjiJAMjvyuiOTQ6FBAmjLbGV7-yUB17Gb2O_2_0xLPo14t-R-SpD06CLP_87t8mb_N2lac9N5bWJ3-6d0kP2MlojEjr7Ag8mjh7L2AkEu418i5heK51c__6v15CgU3rMFRMdj-az8q1QmbTV7wO9h249DEZrUzzz_HrX02uOAde5lbF5Q2cHWGT2HpzUc2nZqaxKS5761EJRZ1G8_H228_Jzat0Dgd7MaNTAhXQs4ErTFLGNY22aiIIUC3U0AjOJdhgS7ujX38gA8EBCfBtDFdYgyaAJ2pJ4_ftUpAQTokMWs-9K4JuqpL0Yy1CcBnLD3j_avXZaYUiA-4KbSJ9hHvvaHohpLZsKRsul1exlZs96Ij9m2FXxBb8wBBjwKL-seTvsLNpVBVR0sZHQOgwFqG3PKVvIYX-C9vzRphXZbUm7cUcWHSCxhUj5YkSgwpaE97Cd9QyGPljPkld2kN2ExyX-Z6IaeAZ0RWylTcEP-uXN8FWnzM1Kl9D1w2Su63NCJuzc-Wrs33dDQBzPXkdupEt8XGbyuWBX5QNng7X2HX7D4kiYcUPHtbUyznvh_9KkVW1xWXc0OvwvFmnuf03OejCJXe6ZrKh_QSVIgYhwlPIRL3ktdLWCJOEZyvZJrvtvVseETXqU8SOljzKK1HEHBoY5GQubOj0hU2pVvbK4OivxQcIYUa6XAaNTf2QE4chmBT_ejaGeNLBlvDDwmblnIVy7SX9ynylrYMcsM1uODP50KCeGkY8hKGC0So2mtinSRYCy21UPWnXG8rbt07bt3X8l4xSqTiHq4vBzFNqA0tXRxmxfk3qf34zeyK4yKCZs5kLqTrn0GUiF4VVn0NwvIU_0qbf8wDp5N5VrepMAztFj6_YTtFYMFv-PbVYNSZtCau7sFzBdF6-W1zQbLIi4GQKz0eyarYM40o1lfUvaFxPSN4O_gr_6BwTCmeRDS-jFGM2PZq0zR7RjNYw39cz6z4k21ifUx3m7jmezDpmNQmt_5KwbaUP1oWq9diiPThb_4497s8U0kgZbGl7Wmh09Yla1I__oi8NBpFIUXNcrJAUsoWfjr5M3N_iDj_RHNfP61oIg2JVL8EcdJ3PPv9k_UkRNYq9WlOBnPP9RKDWbtuZ5Zok4a27qJK9qt8mC43Bnpd4czL5uGKZHTH8GXcL_fhSBl49M9KCePWmMCtWqhl9Ci0wlCT5WIjTOIq-5fbNmlfb4e80TXOPTsKjhuAQevRB0cddGkMoJ2F4ePfm0VwVDFPRoZDjoiDZ8Qm25nTOpLKJYQQZNlHegvsNwVcoR1yrWS3xqZFpud5PtZ1Hmm_RGO5fF6NZUvB9xyDTybMEOvahom8PZ62HaP1WZ3vR7PSworYsfyDXhjDI9PNm4vDZR9dcFj3b2rPo09OtqThO-WITJ9rStHX2gMV9ahwTCk-TGyqj8vpADqF3DuR2MHHi8jBW7uZn9ItmbRa1AWPH9CryjoEmIm4Bs3c2FPOfC3Sogz6Sc7DIj8EV3zBQireNBtPlhC87Je_uq2zpTtnxAjvrDBgg-7SZpda9_XcYunB5mPVvCsoPl07_RpxaKftSpujXGAKp2mrrqvkvkQoqhv8mSADLXl8zHd0ghtn8FG5TpSo1-o0uU6hyMYyXlLTZf3ESqeUIKN7al2srK8sVg7x2zP0pbP-nGyJgxwIIQpyNN8adoZd5NL3LKuhdv5kwOHS7xwFaoQWL-bCtPw5qW2d86xMnE87VJIsuQtewRqYINXl_1pf06UsUokiKrsuIiqXmtsJGX8rnzOhcabnZriEsNAB_wBM45VZ8qh4xpJ8LsbmxCiwE0NVXZ5jYeJRnJQ5tcwJ9ByP0rd-NJ15W5FBXrEvl62Hh_5JrENR6GoVd9tvrdsfmDvuuN2YbWdsG5oHCJXgmrbOQNVTIg3txGVRsXM7vzmCwdfdi7xGrCjak0HeMUFSoDs6Wfkk6mL40FqBIMIPpmhsjw_7GK2skSypm_QzYZULB2wxqX6gYBAoySc0oxq19t8rNPjnRLYCZqcpBU-vzbeY_pFbpKWgtUhWhfzbNYSyl4BVlq8cqI5_YpyHCxkYpwfH6ZQQpWIBwdEyXCrhoPWI9n0kOTm09LjJiiKklSCBtLa5D8UcxFUYWHelram5-vtEmXEObBT0ViOar1Ue7ka7HJahBZ0N278AOecnBMC1GvwXYJvswL2o7KnHr8A6i923e48vY7APyX0SvA6lOJNsN1BZUHE1K5e3yXOE85-Efky10bADge1FTrxcCszbv8cC-SnthV9bPqDTqDDNScIzAIl1xlM92AY9R_1XSH3nowVeRhMHFBMesa4_P7N94cZOyEBJS3b57pfYY9We02h-rCM72NRB5o0g6FVXQYz2hspNmaPqP8aR74CyrwQd6aCDW5jT0ZNojF9jP0dqYiKqzklWpQh92bUYuJkc5tu9kdJYfOGmYycwsm2qiV1DE1eMQRLzSC0fQwcdZd61V2FfX-c0tIPv3JfVU3v2FisRwAZBUyp58gA3Xslow68UquBSv3CVxdVuudo2VkQ9qkQbwDo-8nFN74TJVQNn7Sf-mPmEqFtulGc1-DbMLocGZ2LzKBGJ1BH0zKFjKIF7edfFbw7dyJZvBnhp4K1w2wEj0_cAqF5N8svYV5Nn4twapNxkcl8gmv-CVIFnF7HQhIFZB_jKwxWpJypAt2cza1SN4x938CEjIFyp6xzXim14nt33EAaYnpEy8DpcKqaZs8VNpHfhhtmq6wb3ctsGKemxrH0APumSG1Pja1xfxaU15Gbvu00wVvbJCRVaSBdrdzpnPg8fKr2BpowAoThZsN8uQZqHA0pHf8aBqjL6uqi4pR9VZaelmw4_yt8jXRIhJs0LdNnUpAOmHMhUiLNfHEcmy1iu7x96ewWx_uuZ8D_Q==","summary":[{"type":"summary_text","text":"**Analyzing dispatcher startup race condition**"},{"type":"summary_text","text":"**Identifying stream ring rollover causing classification loss**"}]},{"id":"fc_07547ad47f060e5d016a58ab880b44819abb721cea74ecc2ca","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating network stream ring behavior\",\"pattern\":\"struct StreamLog|impl StreamLog|ring_cap|DEFAULT.*RING|NET.*RING|next_seq|subscribe\\\\(\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_GHhV4t02zs1s3bUeWrJQevCQ","name":"grep"},{"id":"fc_07547ad47f060e5d016a58ab880c78819a8a30002c01bb6eff","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatch restart coverage gap\",\"pattern\":\"run_dispatch_loop|spawn_dispatcher|classify_first_line|worker\\\\(|peek_first_line\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_wbTihobH3jDjIwO88208MKCD","name":"grep"},{"id":"fc_07547ad47f060e5d016a58ab880d00819a93ef5a035e97dc04","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream log implementation\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"180-340,1100-1170\"}","call_id":"call_uMSu7fCb1RQukmVBwNyBiw8W","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab880db8819aaf973a2efc70d902","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach test restart sequence\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"770-920\"}","call_id":"call_nqpr30DC1YrPIEQZGGxIHLS1","name":"read"},{"id":"fc_07547ad47f060e5d016a58ab880dd0819aa4f3d4159e785347","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher tests\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"680-759\"}","call_id":"call_r318rwf13EbOdwGpZLeMSxkD","name":"read"}]},"duration":18435.81220000051,"ttft":3820.8402000004426,"contextSnapshot":{"promptTokens":102216,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GHhV4t02zs1s3bUeWrJQevCQ|fc_07547ad47f060e5d016a58ab880b44819abb721cea74ecc2ca","toolName":"grep","startedAt":"2026-07-16T09:59:36.652Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Locating network stream ring behavior"},"id":"ca93d763","parentId":"a84123dd","timestamp":"2026-07-16T09:59:36.652Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wbTihobH3jDjIwO88208MKCD|fc_07547ad47f060e5d016a58ab880c78819a8a30002c01bb6eff","toolName":"grep","startedAt":"2026-07-16T09:59:36.653Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests"},"intent":"Finding dispatch restart coverage gap"},"id":"86969c2c","parentId":"ca93d763","timestamp":"2026-07-16T09:59:36.653Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_uMSu7fCb1RQukmVBwNyBiw8W|fc_07547ad47f060e5d016a58ab880d00819a93ef5a035e97dc04","toolName":"read","startedAt":"2026-07-16T09:59:36.653Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream log implementation"},"id":"aed867e7","parentId":"86969c2c","timestamp":"2026-07-16T09:59:36.653Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nqpr30DC1YrPIEQZGGxIHLS1|fc_07547ad47f060e5d016a58ab880db8819aaf973a2efc70d902","toolName":"read","startedAt":"2026-07-16T09:59:36.653Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading attach test restart sequence"},"id":"4b21028f","parentId":"aed867e7","timestamp":"2026-07-16T09:59:36.653Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_r318rwf13EbOdwGpZLeMSxkD|fc_07547ad47f060e5d016a58ab880dd0819aa4f3d4159e785347","toolName":"read","startedAt":"2026-07-16T09:59:36.653Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher tests"},"id":"530d5291","parentId":"4b21028f","timestamp":"2026-07-16T09:59:36.653Z"}
{"type":"message","id":"d0885f7e","parentId":"530d5291","timestamp":"2026-07-16T09:59:36.666Z","message":{"role":"toolResult","toolCallId":"call_uMSu7fCb1RQukmVBwNyBiw8W|fc_07547ad47f060e5d016a58ab880d00819a93ef5a035e97dc04","toolName":"read","content":[{"type":"text","text":"struct StreamLog {\n…\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/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline\n/// applied to the conn table itself. `register_conn` appends `connected`; each\n/// connection's closed-watcher appends `disconnected` after removing the row.\n/// A brain subscribes with a cursor and replays events produced while it was\n/// down, so presence survives a brain restart exactly like stream reads do.\n// [impl->REQ-EP-4]\nstruct PresenceLog {\n    next_seq: u64,\n    ring: VecDeque<NetPresenceEvent>,\n    cap_events: usize,\n    subscriber: Option<SharedSend>,\n}\n\nimpl PresenceLog {\n    fn new(cap_events: usize) -> Self {\n        PresenceLog {\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_events,\n            subscriber: None,\n        }\n    }\n\n    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n    /// ring, live-send to the current subscriber. Producers: `register_conn`\n    /// (connected) and the per-conn closed-watchers (disconnected) — every\n    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n    /// in strict `seq` order.\n    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n        self.push(NetPresenceEvent {\n            seq: 0, // assigned in push\n            kind: kind.to_string(),\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            reason: None,\n            remote_addr: serde_json::Value::Null,\n        });\n    }\n\n    /// A `connected` event carrying the connection's OBSERVED remote address\n    /// (REQ-CONV-1): the async-dial write-back seed the blocking `net-dialed`\n    /// reply used to carry — now it rides the presence stream so the pump seeds\n    /// `peer-addrs.json` from a non-blocking dial the same way.\n    // [impl->REQ-CONV-1]\n    fn append_connected(&mut self, conn_id: u64, remote_id_hex: &str, remote_addr: serde_json::Value) {\n…\n    }\n…\n}\n…\n    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {\n…\n        self.bounded_block_on(async move {\n…\n            // semaphore — we never close it.\n            let _permit = sem\n                .acquire_owned()\n                .await\n                .map_err(|e| io::Error::other(e.to_string()))?;\n            plan.connect(addr).await\n        })\n    }\n\n    /// PUMP-TRUTH W2 (REQ-PUMP-PEER-ISOLATION): submit a NON-BLOCKING dial and\n    /// return IMMEDIATELY. Spawns the connect on the host runtime under the shared\n    /// concurrency permit ([`PUMP_DIAL_CONCURRENCY`]); the dial is bounded exactly\n    /// like [`Self::dial`] (W1's per-dial bound), so a dead peer fails PROMPTLY.\n    /// The OUTCOME rides the presence stream — success already emits\n    /// `PRESENCE_CONNECTED` inside `register_conn`; failure appends\n    /// `PRESENCE_DIAL_FAILED` here (conn_id:0 sentinel, correlated by\n    /// `remote_id_hex`). Never a blocking reply, never a whole-round poison — one\n    /// dead peer's fate is isolated to its own event.\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    pub fn submit_dial(&self, addr: EndpointAddr, remote_id_hex: String) {\n        let plan = self.dial_plan();\n        let sem = Arc::clone(&self.dial_semaphore);\n        let shared = Arc::clone(&self.shared);\n        let dur = self.quic_op_timeout();\n        // DIAGNOSTIC (round-3): mark this dial in-flight for the whole task, so\n        // the starvation rig can read how many dead-peer connects are occupying\n        // the 2-worker runtime at once. RAII decrement (panic/cancel-safe).\n        let gauge = Arc::clone(&self.active_dial_tasks);\n        self.runtime.spawn(async move {\n            gauge.fetch_add(1, Ordering::Relaxed);\n            let _dial_guard = DialGauge(gauge);\n            let outcome = async {\n                let _permit = sem\n                    .acquire_owned()\n                    .await\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                match tokio::time::timeout(dur, plan.connect(addr)).await {\n                    Ok(inner) => inner,\n                    Err(_elapsed) => Err(io::Error::other(format!(\n                        \"submit-dial exceeded the {dur:?} bound (peer unresponsive)\"\n                    ))),\n                }\n            }\n            .await;\n            if let Err(e) = outcome {\n                // Ordinary per-peer failure rides the presence stream — the pump\n                // backs THIS peer off and moves on; the round is never poisoned.\n                shared\n                    .presence\n                    .lock()\n                    .unwrap()\n                    .append_dial_failed(&remote_id_hex, e.to_string());\n            }\n        });\n    }\n\n    /// Snapshot the per-dial state (endpoint, shared table, identity, membership,\n    /// the opt-in roster exchange, our live address) into a `Send + 'static`\n    /// [`DialPlan`] both the blocking [`Self::dial`] and the spawned\n    /// [`Self::submit_dial`] connect through — the ONE connect+prove+register\n    /// path, so W1's bound and the seed-proof stay single-sourced.\n    fn dial_plan(&self) -> DialPlan {\n        DialPlan {\n            endpoint: Arc::clone(&self.endpoint),\n            shared: Arc::clone(&self.shared),\n            local_id: self.local_id,\n            membership: self.membership.clone(),\n            roster_rx: self.roster.lock().unwrap().clone(),\n            self_addr: serde_json::to_value(self.endpoint.addr())\n                .unwrap_or(serde_json::Value::Null),\n        }\n    }"}],"details":{"displayContent":{"text":"struct StreamLog {\n…\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/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline\n/// applied to the conn table itself. `register_conn` appends `connected`; each\n/// connection's closed-watcher appends `disconnected` after removing the row.\n/// A brain subscribes with a cursor and replays events produced while it was\n/// down, so presence survives a brain restart exactly like stream reads do.\n// [impl->REQ-EP-4]\nstruct PresenceLog {\n    next_seq: u64,\n    ring: VecDeque<NetPresenceEvent>,\n    cap_events: usize,\n    subscriber: Option<SharedSend>,\n}\n\nimpl PresenceLog {\n    fn new(cap_events: usize) -> Self {\n        PresenceLog {\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_events,\n            subscriber: None,\n        }\n    }\n\n    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n    /// ring, live-send to the current subscriber. Producers: `register_conn`\n    /// (connected) and the per-conn closed-watchers (disconnected) — every\n    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n    /// in strict `seq` order.\n    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n        self.push(NetPresenceEvent {\n            seq: 0, // assigned in push\n            kind: kind.to_string(),\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            reason: None,\n            remote_addr: serde_json::Value::Null,\n        });\n    }\n\n    /// A `connected` event carrying the connection's OBSERVED remote address\n    /// (REQ-CONV-1): the async-dial write-back seed the blocking `net-dialed`\n    /// reply used to carry — now it rides the presence stream so the pump seeds\n    /// `peer-addrs.json` from a non-blocking dial the same way.\n    // [impl->REQ-CONV-1]\n    fn append_connected(&mut self, conn_id: u64, remote_id_hex: &str, remote_addr: serde_json::Value) {\n…\n    }\n…\n}\n…\n    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {\n…\n        self.bounded_block_on(async move {\n…\n            // semaphore — we never close it.\n            let _permit = sem\n                .acquire_owned()\n                .await\n                .map_err(|e| io::Error::other(e.to_string()))?;\n            plan.connect(addr).await\n        })\n    }\n\n    /// PUMP-TRUTH W2 (REQ-PUMP-PEER-ISOLATION): submit a NON-BLOCKING dial and\n    /// return IMMEDIATELY. Spawns the connect on the host runtime under the shared\n    /// concurrency permit ([`PUMP_DIAL_CONCURRENCY`]); the dial is bounded exactly\n    /// like [`Self::dial`] (W1's per-dial bound), so a dead peer fails PROMPTLY.\n    /// The OUTCOME rides the presence stream — success already emits\n    /// `PRESENCE_CONNECTED` inside `register_conn`; failure appends\n    /// `PRESENCE_DIAL_FAILED` here (conn_id:0 sentinel, correlated by\n    /// `remote_id_hex`). Never a blocking reply, never a whole-round poison — one\n    /// dead peer's fate is isolated to its own event.\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    pub fn submit_dial(&self, addr: EndpointAddr, remote_id_hex: String) {\n        let plan = self.dial_plan();\n        let sem = Arc::clone(&self.dial_semaphore);\n        let shared = Arc::clone(&self.shared);\n        let dur = self.quic_op_timeout();\n        // DIAGNOSTIC (round-3): mark this dial in-flight for the whole task, so\n        // the starvation rig can read how many dead-peer connects are occupying\n        // the 2-worker runtime at once. RAII decrement (panic/cancel-safe).\n        let gauge = Arc::clone(&self.active_dial_tasks);\n        self.runtime.spawn(async move {\n            gauge.fetch_add(1, Ordering::Relaxed);\n            let _dial_guard = DialGauge(gauge);\n            let outcome = async {\n                let _permit = sem\n                    .acquire_owned()\n                    .await\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                match tokio::time::timeout(dur, plan.connect(addr)).await {\n                    Ok(inner) => inner,\n                    Err(_elapsed) => Err(io::Error::other(format!(\n                        \"submit-dial exceeded the {dur:?} bound (peer unresponsive)\"\n                    ))),\n                }\n            }\n            .await;\n            if let Err(e) = outcome {\n                // Ordinary per-peer failure rides the presence stream — the pump\n                // backs THIS peer off and moves on; the round is never poisoned.\n                shared\n                    .presence\n                    .lock()\n                    .unwrap()\n                    .append_dial_failed(&remote_id_hex, e.to_string());\n            }\n        });\n    }\n\n    /// Snapshot the per-dial state (endpoint, shared table, identity, membership,\n    /// the opt-in roster exchange, our live address) into a `Send + 'static`\n    /// [`DialPlan`] both the blocking [`Self::dial`] and the spawned\n    /// [`Self::submit_dial`] connect through — the ONE connect+prove+register\n    /// path, so W1's bound and the seed-proof stay single-sourced.\n    fn dial_plan(&self) -> DialPlan {\n        DialPlan {\n            endpoint: Arc::clone(&self.endpoint),\n            shared: Arc::clone(&self.shared),\n            local_id: self.local_id,\n            membership: self.membership.clone(),\n            roster_rx: self.roster.lock().unwrap().clone(),\n            self_addr: serde_json::to_value(self.endpoint.addr())\n                .unwrap_or(serde_json::Value::Null),\n        }\n    }","startLine":158,"lineNumbers":[158,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,null,349,null,412,null,1090,null,1096,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784195976666}}
{"type":"message","id":"e37fd8e4","parentId":"d0885f7e","timestamp":"2026-07-16T09:59:36.672Z","message":{"role":"toolResult","toolCallId":"call_GHhV4t02zs1s3bUeWrJQevCQ|fc_07547ad47f060e5d016a58ab880b44819abb721cea74ecc2ca","toolName":"grep","content":[{"type":"text","text":" 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 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*163|    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\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 169|    ring: VecDeque<(u64, Vec<u8>)>,\n 170|    cap_chunks: usize,\n 171|    subscriber: Option<SharedSend>,\n...\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...\n 210|    fn append(&mut self, bytes: &[u8]) {\n*211|        let seq = self.next_seq;\n*212|        self.next_seq += 1;\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...\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...\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...\n 302|struct PresenceLog {\n*303|    next_seq: u64,\n 304|    ring: VecDeque<NetPresenceEvent>,\n 305|    cap_events: usize,\n 306|    subscriber: Option<SharedSend>,\n...\n 311|        PresenceLog {\n*312|            next_seq: 0,\n 313|            ring: VecDeque::new(),\n 314|            cap_events,\n 315|            subscriber: None,\n...\n 369|    fn push(&mut self, mut ev: NetPresenceEvent) {\n*370|        ev.seq = self.next_seq;\n*371|        self.next_seq += 1;\n 372|        self.ring.push_back(ev.clone());\n 373|        while self.ring.len() > self.cap_events {\n 374|            self.ring.pop_front();\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 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...\n 1612|\n*1613|    // [unit->REQ-HAZARD-RESTART-IDEMPOTENT] StreamLog.next_seq counts read-pump\n 1614|    // APPENDS (transport reads), NOT logical sender chunks: each append() bumps it\n 1615|    // by one regardless of byte size. Under QUIC coalescing, N logical sender\n 1616|    // writes can arrive as M < N appends (one read_chunk carrying several writes'\n*1617|    // bytes), so next_seq == M, not N. This documents WHY the netstream int test\n 1618|    // must gate on BYTES (byte-exact + exactly-once), never on\n 1619|    // seq == logical-chunk-count (the Windows coalescing flake). The bytes are\n 1620|    // retained + drained in arrival order, independent of the seq count.\n 1621|    #[test]\n*1622|    fn next_seq_counts_appends_not_logical_chunks() {\n 1623|        let mut log = StreamLog::new(7, 1024);\n*1624|        assert_eq!(log.next_seq, 0);\n 1625|        // Two appends of wildly different sizes (a coalesced read carrying many\n*1626|        // logical writes vs a 1-byte read) each bump next_seq by exactly one.\n 1627|        log.append(b\"x\");\n 1628|        log.append(b\"a-much-larger-coalesced-read-carrying-many-logical-sender-writes\");\n 1629|        assert_eq!(\n*1630|            log.next_seq, 2,\n*1631|            \"two appends -> next_seq 2, regardless of per-append byte size\"\n 1632|        );\n 1633|        // The BYTES are retained + drained in arrival order, framing-agnostic.\n 1634|        let (bytes, _finished) = log.drain();\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;"}],"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":28,"fileCount":1,"files":["crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":28}],"truncated":false,"displayContent":"  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  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 *163│    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\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  169│    ring: VecDeque<(u64, Vec<u8>)>,\n  170│    cap_chunks: usize,\n  171│    subscriber: Option<SharedSend>,\n     │...\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     │...\n  210│    fn append(&mut self, bytes: &[u8]) {\n *211│        let seq = self.next_seq;\n *212│        self.next_seq += 1;\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     │...\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     │...\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     │...\n  302│struct PresenceLog {\n *303│    next_seq: u64,\n  304│    ring: VecDeque<NetPresenceEvent>,\n  305│    cap_events: usize,\n  306│    subscriber: Option<SharedSend>,\n     │...\n  311│        PresenceLog {\n *312│            next_seq: 0,\n  313│            ring: VecDeque::new(),\n  314│            cap_events,\n  315│            subscriber: None,\n     │...\n  369│    fn push(&mut self, mut ev: NetPresenceEvent) {\n *370│        ev.seq = self.next_seq;\n *371│        self.next_seq += 1;\n  372│        self.ring.push_back(ev.clone());\n  373│        while self.ring.len() > self.cap_events {\n  374│            self.ring.pop_front();\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 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     │...\n 1612│\n*1613│    // [unit->REQ-HAZARD-RESTART-IDEMPOTENT] StreamLog.next_seq counts read-pump\n 1614│    // APPENDS (transport reads), NOT logical sender chunks: each append() bumps it\n 1615│    // by one regardless of byte size. Under QUIC coalescing, N logical sender\n 1616│    // writes can arrive as M < N appends (one read_chunk carrying several writes'\n*1617│    // bytes), so next_seq == M, not N. This documents WHY the netstream int test\n 1618│    // must gate on BYTES (byte-exact + exactly-once), never on\n 1619│    // seq == logical-chunk-count (the Windows coalescing flake). The bytes are\n 1620│    // retained + drained in arrival order, independent of the seq count.\n 1621│    #[test]\n*1622│    fn next_seq_counts_appends_not_logical_chunks() {\n 1623│        let mut log = StreamLog::new(7, 1024);\n*1624│        assert_eq!(log.next_seq, 0);\n 1625│        // Two appends of wildly different sizes (a coalesced read carrying many\n*1626│        // logical writes vs a 1-byte read) each bump next_seq by exactly one.\n 1627│        log.append(b\"x\");\n 1628│        log.append(b\"a-much-larger-coalesced-read-carrying-many-logical-sender-writes\");\n 1629│        assert_eq!(\n*1630│            log.next_seq, 2,\n*1631│            \"two appends -> next_seq 2, regardless of per-append byte size\"\n 1632│        );\n 1633│        // The BYTES are retained + drained in arrival order, framing-agnostic.\n 1634│        let (bytes, _finished) = log.drain();\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;"},"isError":false,"timestamp":1784195976672}}
{"type":"message","id":"8f6a261d","parentId":"e37fd8e4","timestamp":"2026-07-16T09:59:36.674Z","message":{"role":"toolResult","toolCallId":"call_wbTihobH3jDjIwO88208MKCD|fc_07547ad47f060e5d016a58ab880c78819a8a30002c01bb6eff","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/tests/\n## dispatch.rs\n 7|//! Topology per test: two hermetic loopback brokers (requester A, responder\n*8|//! B). B runs `run_dispatch_loop` like the real daemon; B's gates load from\n 9|//! the **canonical** SPT_HOME stores (the production path), so each test\n 10|//! seeds subnet/trust/visibility files first. Tests serialize on one lock —\n 11|//! they share the process's SPT_HOME.\n...\n 20|use spt_daemon::effect::{Minter, MintedOp};\n*21|use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\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...\n 96|/// flag (set on test exit so the loop thread winds down).\n*97|fn spawn_dispatcher_for(broker_name: &str, scratch: &std::path::Path) -> Arc<AtomicBool> {\n 98|    let stop = Arc::new(AtomicBool::new(false));\n 99|    let flag = Arc::clone(&stop);\n 100|    let name = broker_name.to_string();\n...\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 167|    let _b = net_broker(&b_name, dir.path());\n*168|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 169|\n 170|    // B's canonical context store holds the mind A will pull.\n 171|    let id = \"ling-d91-sync\";\n...\n 218|    let _b = net_broker(&b_name, dir.path());\n*219|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 220|\n 221|    // Stage v6 at B (in the canonical releases dir the dispatcher opens).\n 222|    use ed25519_dalek::{Signer, SigningKey};\n...\n 295|    let _b = net_broker(&b_name, dir.path());\n*296|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 297|\n 298|    let mut a = connect_retry(&a_name);\n 299|    let mut b_probe = connect_retry(&b_name);\n...\n 353|    let _b = net_broker(&b_name, dir.path());\n*354|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 355|\n 356|    // An offline perch at B so the funnel takes the atomic spool path.\n 357|    let target = \"ling-d91-wan\";\n...\n 427|    let _b = net_broker(&b_name, dir.path());\n*428|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 429|\n 430|    // B hosts one labeled echo session and one unlabeled one.\n 431|    #[cfg(unix)]\n...\n 539|    let _b = net_broker(&b_name, dir.path());\n*540|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 541|\n 542|    // B hosts a live labeled echo session — but A is NOT told its session id.\n 543|    #[cfg(unix)]\n...\n 669|    let _b = net_broker(&b_name, dir.path());\n*670|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 671|\n 672|    // The served file lives under B's canonical transfer root.\n 673|    let root = spt_store::xfer::transfers_dir();\n...\n 718|    let _b = net_broker(&b_name, dir.path());\n*719|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 720|\n 721|    let mut a = connect_retry(&a_name);\n 722|    let mut b_probe = connect_retry(&b_name);\n...\n 870|    let _b = net_broker(&b_name, dir.path());\n*871|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 872|\n 873|    // \"B\"'s side: a registered trivial shell adapter + the owner's minted,\n 874|    // offline persistent instance (the canonical home is shared in-proc —\n...\n 985|    let _b = net_broker(&b_name, dir.path());\n*986|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-probe\"));\n 987|\n 988|    // B holds subnet \"ProbeNet\", attached by default (the canonical SPT_HOME\n 989|    // stores are what B's dispatcher consults — the production path).\n...\n 1044|fn classify_routes_a_node_label_feed_to_registry() {\n*1045|    use spt_daemon::dispatch::{classify_first_line, StreamFamily};\n 1046|    use spt_net::net::replicate::{NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate};\n 1047|    use spt_net::net::registry::{Instance, Status};\n 1048|\n...\n 1057|    assert_eq!(\n*1058|        classify_first_line(&line),\n 1059|        StreamFamily::Registry,\n 1060|        \"an endpoint-less node's label-first feed routes to Registry\"\n 1061|    );\n...\n 1085|        serde_json::from_slice(&inst.encode_line()[..inst.encode_line().len() - 1]).unwrap();\n*1086|    assert_eq!(classify_first_line(&iline), StreamFamily::Registry);\n 1087|\n 1088|    let junk: serde_json::Value = serde_json::json!({\"hello\": \"world\"});\n*1089|    assert_eq!(classify_first_line(&junk), StreamFamily::Unknown);\n 1090|}\n 1091|\n 1092|// [unit->REQ-NET-1] an unclassifiable stream is dropped without wedging the\n...\n 1101|    let _b = net_broker(&b_name, dir.path());\n*1102|    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 1103|\n 1104|    let mut a = connect_retry(&a_name);\n 1105|    let mut b_probe = connect_retry(&b_name);\n## mesh.rs\n 19|//! per-node `PumpPaths`, the roster as the push target) and the **gate side\n*20|//! runs the real dispatch loop** (`run_dispatch_loop`) under a permissive\n 21|//! canonical `SPT_HOME` roster. Addresses are omniscient (a node still only\n 22|//! *pushes* where its roster says); address *discovery* is the roster's job,\n 23|//! tested at D3/D4.\n...\n 37|use spt_daemon::broker::Broker;\n*38|use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n 39|use spt_daemon::nethost::{NetConfig, NetHost};\n 40|use spt_daemon::pump::{\n 41|    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,\n...\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 17|use spt_daemon::broker::Broker;\n*18|use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n 19|use spt_daemon::nethost::{NetConfig, NetHost};\n 20|use spt_daemon::pump::{\n 21|    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,\n...\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## twohost.rs\n 63|use spt_daemon::effect::{Minter, MintedOp};\n*64|use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n 65|use spt_daemon::nethost::{NetConfig, NetHost};\n 66|use spt_daemon::pump::{\n 67|    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,\n...\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|    {"}],"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":26,"fileCount":4,"files":["crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/mesh.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/twohost.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/dispatch.rs","count":19},{"path":"crates/spt-daemon/tests/mesh.rs","count":3},{"path":"crates/spt-daemon/tests/pump.rs","count":2},{"path":"crates/spt-daemon/tests/twohost.rs","count":2}],"truncated":false,"displayContent":"# crates/spt-daemon/tests/\n## dispatch.rs\n    7│//! Topology per test: two hermetic loopback brokers (requester A, responder\n   *8│//! B). B runs `run_dispatch_loop` like the real daemon; B's gates load from\n    9│//! the **canonical** SPT_HOME stores (the production path), so each test\n   10│//! seeds subnet/trust/visibility files first. Tests serialize on one lock —\n   11│//! they share the process's SPT_HOME.\n     │...\n   20│use spt_daemon::effect::{Minter, MintedOp};\n  *21│use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\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     │...\n   96│/// flag (set on test exit so the loop thread winds down).\n  *97│fn spawn_dispatcher_for(broker_name: &str, scratch: &std::path::Path) -> Arc<AtomicBool> {\n   98│    let stop = Arc::new(AtomicBool::new(false));\n   99│    let flag = Arc::clone(&stop);\n  100│    let name = broker_name.to_string();\n     │...\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  167│    let _b = net_broker(&b_name, dir.path());\n *168│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  169│\n  170│    // B's canonical context store holds the mind A will pull.\n  171│    let id = \"ling-d91-sync\";\n     │...\n  218│    let _b = net_broker(&b_name, dir.path());\n *219│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  220│\n  221│    // Stage v6 at B (in the canonical releases dir the dispatcher opens).\n  222│    use ed25519_dalek::{Signer, SigningKey};\n     │...\n  295│    let _b = net_broker(&b_name, dir.path());\n *296│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  297│\n  298│    let mut a = connect_retry(&a_name);\n  299│    let mut b_probe = connect_retry(&b_name);\n     │...\n  353│    let _b = net_broker(&b_name, dir.path());\n *354│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  355│\n  356│    // An offline perch at B so the funnel takes the atomic spool path.\n  357│    let target = \"ling-d91-wan\";\n     │...\n  427│    let _b = net_broker(&b_name, dir.path());\n *428│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  429│\n  430│    // B hosts one labeled echo session and one unlabeled one.\n  431│    #[cfg(unix)]\n     │...\n  539│    let _b = net_broker(&b_name, dir.path());\n *540│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  541│\n  542│    // B hosts a live labeled echo session — but A is NOT told its session id.\n  543│    #[cfg(unix)]\n     │...\n  669│    let _b = net_broker(&b_name, dir.path());\n *670│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  671│\n  672│    // The served file lives under B's canonical transfer root.\n  673│    let root = spt_store::xfer::transfers_dir();\n     │...\n  718│    let _b = net_broker(&b_name, dir.path());\n *719│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  720│\n  721│    let mut a = connect_retry(&a_name);\n  722│    let mut b_probe = connect_retry(&b_name);\n     │...\n  870│    let _b = net_broker(&b_name, dir.path());\n *871│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n  872│\n  873│    // \"B\"'s side: a registered trivial shell adapter + the owner's minted,\n  874│    // offline persistent instance (the canonical home is shared in-proc —\n     │...\n  985│    let _b = net_broker(&b_name, dir.path());\n *986│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-probe\"));\n  987│\n  988│    // B holds subnet \"ProbeNet\", attached by default (the canonical SPT_HOME\n  989│    // stores are what B's dispatcher consults — the production path).\n     │...\n 1044│fn classify_routes_a_node_label_feed_to_registry() {\n*1045│    use spt_daemon::dispatch::{classify_first_line, StreamFamily};\n 1046│    use spt_net::net::replicate::{NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate};\n 1047│    use spt_net::net::registry::{Instance, Status};\n 1048│\n     │...\n 1057│    assert_eq!(\n*1058│        classify_first_line(&line),\n 1059│        StreamFamily::Registry,\n 1060│        \"an endpoint-less node's label-first feed routes to Registry\"\n 1061│    );\n     │...\n 1085│        serde_json::from_slice(&inst.encode_line()[..inst.encode_line().len() - 1]).unwrap();\n*1086│    assert_eq!(classify_first_line(&iline), StreamFamily::Registry);\n 1087│\n 1088│    let junk: serde_json::Value = serde_json::json!({\"hello\": \"world\"});\n*1089│    assert_eq!(classify_first_line(&junk), StreamFamily::Unknown);\n 1090│}\n 1091│\n 1092│// [unit->REQ-NET-1] an unclassifiable stream is dropped without wedging the\n     │...\n 1101│    let _b = net_broker(&b_name, dir.path());\n*1102│    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n 1103│\n 1104│    let mut a = connect_retry(&a_name);\n 1105│    let mut b_probe = connect_retry(&b_name);\n## mesh.rs\n  19│//! per-node `PumpPaths`, the roster as the push target) and the **gate side\n *20│//! runs the real dispatch loop** (`run_dispatch_loop`) under a permissive\n  21│//! canonical `SPT_HOME` roster. Addresses are omniscient (a node still only\n  22│//! *pushes* where its roster says); address *discovery* is the roster's job,\n  23│//! tested at D3/D4.\n    │...\n  37│use spt_daemon::broker::Broker;\n *38│use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n  39│use spt_daemon::nethost::{NetConfig, NetHost};\n  40│use spt_daemon::pump::{\n  41│    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,\n    │...\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  17│use spt_daemon::broker::Broker;\n *18│use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n  19│use spt_daemon::nethost::{NetConfig, NetHost};\n  20│use spt_daemon::pump::{\n  21│    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,\n    │...\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## twohost.rs\n  63│use spt_daemon::effect::{Minter, MintedOp};\n *64│use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n  65│use spt_daemon::nethost::{NetConfig, NetHost};\n  66│use spt_daemon::pump::{\n  67│    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,\n    │...\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│    {"},"isError":false,"timestamp":1784195976674}}
{"type":"message","id":"ab444cd9","parentId":"8f6a261d","timestamp":"2026-07-16T09:59:36.679Z","message":{"role":"toolResult","toolCallId":"call_r318rwf13EbOdwGpZLeMSxkD|fc_07547ad47f060e5d016a58ab880dd0819aa4f3d4159e785347","toolName":"read","content":[{"type":"text","text":"fn serve_wan_feed(\n…\n) -> DispatchOutcome {\n…\n    loop {\n        match brain.read_event() {\n…\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Spawn the dispatcher on its own thread against the canonical broker\n/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\npub fn spawn_dispatcher(\n    broker_name: String,\n    registry: Arc<RegistryHost>,\n    stop: Arc<AtomicBool>,\n) -> thread::JoinHandle<()> {\n    thread::spawn(move || {\n        let paths = DispatchPaths::production();\n        if let Err(e) =\n            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n        {\n            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n\n    // [unit->REQ-NET-1] the demux table: every family's first record\n    // classifies correctly, including the `request` kind collision.\n    #[test]\n    fn classifies_every_family_by_first_line_shape() {\n        let sync = json!({\"kind\": \"request\", \"sync_id\": \"pull-1\", \"refs\": [], \"have_tips\": {}});\n        assert_eq!(classify_first_line(&sync), StreamFamily::Sync);\n\n        let attach = json!({\"kind\": \"request\", \"session_id\": 3, \"from_seq\": 0});\n        assert_eq!(classify_first_line(&attach), StreamFamily::Attach);\n\n        // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.\n        let rest =\n            json!({\"kind\": \"request\", \"endpoint\": \"ling\", \"event\": \"suspend\", \"op_id\": \"a:1\"});\n        assert_eq!(classify_first_line(&rest), StreamFamily::Rest);\n\n        let update =\n            json!({\"kind\": \"query\", \"upd_id\": \"u1\", \"current_version\": 5, \"channel\": \"stable\"});\n        assert_eq!(classify_first_line(&update), StreamFamily::Update);\n\n        let notif = json!({\"kind\": \"row\", \"row\": {}});\n        assert_eq!(classify_first_line(&notif), StreamFamily::Notif);\n\n        let fetch = json!({\"kind\": \"fetch\", \"path\": \"a.txt\", \"xfer_id\": \"x1\"});\n        assert_eq!(classify_first_line(&fetch), StreamFamily::Xfer);\n        let push = json!({\"kind\": \"push\", \"path\": \"a.txt\", \"total\": 4, \"xfer_id\": \"x2\"});\n        assert_eq!(classify_first_line(&push), StreamFamily::Xfer);\n\n        let wan = json!({\"target\": \"ling\", \"from\": \"oak\", \"body\": \"hi\", \"op_id\": \"w1\"});\n        assert_eq!(classify_first_line(&wan), StreamFamily::WanMsg);\n\n        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n    }\n\n    // [unit->REQ-NET-1] unclassifiable shapes are Unknown — dropped loud,\n    // never routed to a gate-bearing serve loop on a guess.\n    #[test]\n    fn unknown_shapes_refuse_classification() {\n        for v in [\n            json!({\"kind\": \"chunk\", \"sync_id\": \"s\", \"offset\": 0, \"data_b64\": \"\"}), // mid-protocol, not an opener\n            json!({\"kind\": \"request\"}), // request without any discriminant\n            json!({\"kind\": \"request\", \"endpoint\": \"ling\"}), // rest-ish but no event\n            json!({\"kind\": \"nonsense\"}),\n            json!({\"target\": \"ling\"}), // wan-ish but no op_id\n            json!({\"subnet\": \"home\"}), // registry-ish but no instance\n            json!({\"completely\": \"other\"}),\n            json!(42),\n        ] {\n            assert_eq!(classify_first_line(&v), StreamFamily::Unknown, \"for {v}\");\n        }\n    }\n}\n"}],"details":{"displayContent":{"text":"fn serve_wan_feed(\n…\n) -> DispatchOutcome {\n…\n    loop {\n        match brain.read_event() {\n…\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\r\n        }\r\n    }\r\n}\r\n\r\n/// Spawn the dispatcher on its own thread against the canonical broker\r\n/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\r\npub fn spawn_dispatcher(\r\n    broker_name: String,\r\n    registry: Arc<RegistryHost>,\r\n    stop: Arc<AtomicBool>,\r\n) -> thread::JoinHandle<()> {\r\n    thread::spawn(move || {\r\n        let paths = DispatchPaths::production();\r\n        if let Err(e) =\r\n            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\r\n        {\r\n            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\r\n        }\r\n    })\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use serde_json::json;\r\n\r\n    // [unit->REQ-NET-1] the demux table: every family's first record\r\n    // classifies correctly, including the `request` kind collision.\r\n    #[test]\r\n    fn classifies_every_family_by_first_line_shape() {\r\n        let sync = json!({\"kind\": \"request\", \"sync_id\": \"pull-1\", \"refs\": [], \"have_tips\": {}});\r\n        assert_eq!(classify_first_line(&sync), StreamFamily::Sync);\r\n\r\n        let attach = json!({\"kind\": \"request\", \"session_id\": 3, \"from_seq\": 0});\r\n        assert_eq!(classify_first_line(&attach), StreamFamily::Attach);\r\n\r\n        // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.\r\n        let rest =\r\n            json!({\"kind\": \"request\", \"endpoint\": \"ling\", \"event\": \"suspend\", \"op_id\": \"a:1\"});\r\n        assert_eq!(classify_first_line(&rest), StreamFamily::Rest);\r\n\r\n        let update =\r\n            json!({\"kind\": \"query\", \"upd_id\": \"u1\", \"current_version\": 5, \"channel\": \"stable\"});\r\n        assert_eq!(classify_first_line(&update), StreamFamily::Update);\r\n\r\n        let notif = json!({\"kind\": \"row\", \"row\": {}});\r\n        assert_eq!(classify_first_line(&notif), StreamFamily::Notif);\r\n\r\n        let fetch = json!({\"kind\": \"fetch\", \"path\": \"a.txt\", \"xfer_id\": \"x1\"});\r\n        assert_eq!(classify_first_line(&fetch), StreamFamily::Xfer);\r\n        let push = json!({\"kind\": \"push\", \"path\": \"a.txt\", \"total\": 4, \"xfer_id\": \"x2\"});\r\n        assert_eq!(classify_first_line(&push), StreamFamily::Xfer);\r\n\r\n        let wan = json!({\"target\": \"ling\", \"from\": \"oak\", \"body\": \"hi\", \"op_id\": \"w1\"});\r\n        assert_eq!(classify_first_line(&wan), StreamFamily::WanMsg);\r\n\r\n        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\r\n            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\r\n        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\r\n    }\r\n\r\n    // [unit->REQ-NET-1] unclassifiable shapes are Unknown — dropped loud,\r\n    // never routed to a gate-bearing serve loop on a guess.\r\n    #[test]\r\n    fn unknown_shapes_refuse_classification() {\r\n        for v in [\r\n            json!({\"kind\": \"chunk\", \"sync_id\": \"s\", \"offset\": 0, \"data_b64\": \"\"}), // mid-protocol, not an opener\r\n            json!({\"kind\": \"request\"}), // request without any discriminant\r\n            json!({\"kind\": \"request\", \"endpoint\": \"ling\"}), // rest-ish but no event\r\n            json!({\"kind\": \"nonsense\"}),\r\n            json!({\"target\": \"ling\"}), // wan-ish but no op_id\r\n            json!({\"subnet\": \"home\"}), // registry-ish but no instance\r\n            json!({\"completely\": \"other\"}),\r\n            json!(42),\r\n        ] {\r\n            assert_eq!(classify_first_line(&v), StreamFamily::Unknown, \"for {v}\");\r\n        }\r\n    }\r\n}\r\n","startLine":631,"lineNumbers":[631,null,637,null,644,645,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784195976679}}
{"type":"message","id":"d9e8c787","parentId":"ab444cd9","timestamp":"2026-07-16T09:59:36.688Z","message":{"role":"toolResult","toolCallId":"call_nqpr30DC1YrPIEQZGGxIHLS1|fc_07547ad47f060e5d016a58ab880db8819aaf973a2efc70d902","toolName":"read","content":[{"type":"text","text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\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[Showing lines 769-923 of 1550. Use :924 to continue]"}],"details":{"truncation":{"content":"    brain.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\r\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\r\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\r\n// dedup through the journals, input B sent INTO the dead window arrives, and\r\n// B's viewport holds every marker exactly once.\r\n// [unit->REQ-INST-8]\r\n#[test]\r\nfn attach_survives_target_brain_restart_exactly_once() {\r\n    init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let (name_a, name_b) = (unique_name(), unique_name());\r\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\r\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\r\n\r\n    // Target A life 1: spawn the child; operator B dials + requests + types.\r\n    let mut life1 = connect_retry(&name_a);\r\n    let sid = life1\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n    let a_addr = life1.net_status().expect(\"a status\").addr;\r\n\r\n    let mut operator = connect_retry(&name_b);\r\n    let conn = operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\r\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_ONE\\r\", 10).expect(\"input 1\");\r\n\r\n    // Life 1 serves manually (the serve loop's exact steps, stepped so the\r\n    // crash point is deterministic): handle the Request + input, pump output\r\n    // records until MARKER_ONE went onto the wire — then die.\r\n    let (stream_a, origin) = wait_for_stream(&mut life1);\r\n    life1\r\n        .net_stream_subscribe(stream_a, 0)\r\n        .expect(\"life1 subscribe\");\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\r\n    'life1: loop {\r\n        match life1.read_event().expect(\"life1 event\") {\r\n            BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == stream_a => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            ..\r\n                        } => {\r\n                            life1.attach(session_id, from_seq).expect(\"attach\");\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id } => {\r\n                            let b = decode_bytes(&data_b64).expect(\"b64\");\r\n                            life1.send_effect(MintedOp::new(Minter::Rc, op_id), &b).expect(\"journaled input\");\r\n                        }\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n            BrokerEvent::Output { seq, bytes, .. } => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\r\n                });\r\n                life1\r\n                    .net_stream_send(stream_a, &line, None, false)\r\n                    .expect(\"output send\");\r\n                sent.extend_from_slice(&bytes);\r\n                if count(&sent, b\"MARKER_ONE\") >= 1 {\r\n                    break 'life1; // crash with the marker on the wire\r\n                }\r\n            }\r\n            BrokerEvent::Error { message } => panic!(\"life1 broker error: {message}\"),\r\n            _ => {}\r\n        }\r\n    }\r\n    let state = life1.snapshot();\r\n    drop(life1); // the crash — broker A, session, stream, journal all survive\r\n\r\n    // Into the DEAD window: B types the second marker (buffered by A's broker).\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\r\n\r\n    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\r\n    // worst-case replay. serve_attach re-handles the replayed Request (gate +\r\n    // re-attach from 0, so every output record re-transmits — B's render\r\n    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\r\n    // journal dedups op 10 — typed once), and pumps the dead window's\r\n    // MARKER_TWO.\r\n    let mut life2 = Brain::handoff(&name_a, state, 2).expect(\"handoff\");\r\n    let origin2 = origin.clone();\r\n    let serve_broker = name_a.clone();\r\n    let server = thread::spawn(move || {\r\n        let outcome = serve_attach(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\r\n            .expect(\"re-serve\");\r\n        (outcome, life2)\r\n    });\r\n\r\n    // B renders through the dead-window marker, then detaches.\r\n    let mut op_decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut op_decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"MARKER_TWO\",\r\n    );\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let (outcome, mut life2) = server.join().expect(\"serve thread\");\r\n    assert_eq!(outcome, AttachServeOutcome::Detached);\r\n\r\n    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\r\n    // longer replays the raw ring — the broker synthesizes a clean current-screen\r\n    // repaint — so the old \"remote wire byte-identical to the raw local log\" proxy\r\n    // no longer holds (that raw replay is exactly what #6 removed). Instead we feed\r\n    // the operator's ENTIRE received byte stream (early live frames + the\r\n    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\r\n    // the resulting authoritative screen still shows BOTH markers. The screen is the\r\n    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\r\n    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\r\n    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\r\n    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\r\n    // makes this robust to the mid-stream repaint's clear/repaint bytes.\r\n    let mut screen = ScreenGrid::new(24, 80);\r\n    screen.advance(&viewport);\r\n    let painted = String::from_utf8_lossy(&screen.render_repaint()).into_owned();\r\n    assert!(\r\n        painted.contains(\"MARKER_ONE\"),\r\n        \"the operator's rendered screen still shows the pre-crash marker across the \\\r\n         brain restart (no gap in the re-served output): {painted:?}\"\r\n    );\r\n    assert!(\r\n        painted.contains(\"MARKER_TWO\"),\r\n        \"the operator's rendered screen shows the dead-window marker delivered after \\\r\n         the handoff (buffered input survived the restart): {painted:?}\"\r\n    );\r\n\r\n    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\r\n    // repaint) also carries both markers exactly-once at the source.\r\n    life2.attach(sid, 0).expect(\"local ground-truth attach\");\r\n    let mut local: Vec<u8> = Vec::new();\r\n    life2\r\n        .read_output_until(b\"MARKER_TWO\", &mut local)\r\n        .expect(\"local replay\");\r\n    assert!(count(&local, b\"MARKER_ONE\") >= 1 && count(&local, b\"MARKER_TWO\") >= 1);\r\n\r\n    life2.kill_session().expect(\"cleanup kill\");\r\n}\r","truncated":true,"truncatedBy":"lines","totalLines":782,"totalBytes":7451,"outputLines":155,"outputBytes":7451,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    brain.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\r\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\r\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\r\n// dedup through the journals, input B sent INTO the dead window arrives, and\r\n// B's viewport holds every marker exactly once.\r\n// [unit->REQ-INST-8]\r\n#[test]\r\nfn attach_survives_target_brain_restart_exactly_once() {\r\n    init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let (name_a, name_b) = (unique_name(), unique_name());\r\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\r\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\r\n\r\n    // Target A life 1: spawn the child; operator B dials + requests + types.\r\n    let mut life1 = connect_retry(&name_a);\r\n    let sid = life1\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n    let a_addr = life1.net_status().expect(\"a status\").addr;\r\n\r\n    let mut operator = connect_retry(&name_b);\r\n    let conn = operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\r\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_ONE\\r\", 10).expect(\"input 1\");\r\n\r\n    // Life 1 serves manually (the serve loop's exact steps, stepped so the\r\n    // crash point is deterministic): handle the Request + input, pump output\r\n    // records until MARKER_ONE went onto the wire — then die.\r\n    let (stream_a, origin) = wait_for_stream(&mut life1);\r\n    life1\r\n        .net_stream_subscribe(stream_a, 0)\r\n        .expect(\"life1 subscribe\");\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\r\n    'life1: loop {\r\n        match life1.read_event().expect(\"life1 event\") {\r\n            BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == stream_a => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            ..\r\n                        } => {\r\n                            life1.attach(session_id, from_seq).expect(\"attach\");\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id } => {\r\n                            let b = decode_bytes(&data_b64).expect(\"b64\");\r\n                            life1.send_effect(MintedOp::new(Minter::Rc, op_id), &b).expect(\"journaled input\");\r\n                        }\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n            BrokerEvent::Output { seq, bytes, .. } => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\r\n                });\r\n                life1\r\n                    .net_stream_send(stream_a, &line, None, false)\r\n                    .expect(\"output send\");\r\n                sent.extend_from_slice(&bytes);\r\n                if count(&sent, b\"MARKER_ONE\") >= 1 {\r\n                    break 'life1; // crash with the marker on the wire\r\n                }\r\n            }\r\n            BrokerEvent::Error { message } => panic!(\"life1 broker error: {message}\"),\r\n            _ => {}\r\n        }\r\n    }\r\n    let state = life1.snapshot();\r\n    drop(life1); // the crash — broker A, session, stream, journal all survive\r\n\r\n    // Into the DEAD window: B types the second marker (buffered by A's broker).\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\r\n\r\n    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\r\n    // worst-case replay. serve_attach re-handles the replayed Request (gate +\r\n    // re-attach from 0, so every output record re-transmits — B's render\r\n    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\r\n    // journal dedups op 10 — typed once), and pumps the dead window's\r\n    // MARKER_TWO.\r\n    let mut life2 = Brain::handoff(&name_a, state, 2).expect(\"handoff\");\r\n    let origin2 = origin.clone();\r\n    let serve_broker = name_a.clone();\r\n    let server = thread::spawn(move || {\r\n        let outcome = serve_attach(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\r\n            .expect(\"re-serve\");\r\n        (outcome, life2)\r\n    });\r\n\r\n    // B renders through the dead-window marker, then detaches.\r\n    let mut op_decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut op_decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"MARKER_TWO\",\r\n    );\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let (outcome, mut life2) = server.join().expect(\"serve thread\");\r\n    assert_eq!(outcome, AttachServeOutcome::Detached);\r\n\r\n    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\r\n    // longer replays the raw ring — the broker synthesizes a clean current-screen\r\n    // repaint — so the old \"remote wire byte-identical to the raw local log\" proxy\r\n    // no longer holds (that raw replay is exactly what #6 removed). Instead we feed\r\n    // the operator's ENTIRE received byte stream (early live frames + the\r\n    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\r\n    // the resulting authoritative screen still shows BOTH markers. The screen is the\r\n    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\r\n    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\r\n    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\r\n    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\r\n    // makes this robust to the mid-stream repaint's clear/repaint bytes.\r\n    let mut screen = ScreenGrid::new(24, 80);\r\n    screen.advance(&viewport);\r\n    let painted = String::from_utf8_lossy(&screen.render_repaint()).into_owned();\r\n    assert!(\r\n        painted.contains(\"MARKER_ONE\"),\r\n        \"the operator's rendered screen still shows the pre-crash marker across the \\\r\n         brain restart (no gap in the re-served output): {painted:?}\"\r\n    );\r\n    assert!(\r\n        painted.contains(\"MARKER_TWO\"),\r\n        \"the operator's rendered screen shows the dead-window marker delivered after \\\r\n         the handoff (buffered input survived the restart): {painted:?}\"\r\n    );\r\n\r\n    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\r\n    // repaint) also carries both markers exactly-once at the source.\r\n    life2.attach(sid, 0).expect(\"local ground-truth attach\");\r\n    let mut local: Vec<u8> = Vec::new();\r\n    life2\r\n        .read_output_until(b\"MARKER_TWO\", &mut local)\r\n        .expect(\"local replay\");\r\n    assert!(count(&local, b\"MARKER_ONE\") >= 1 && count(&local, b\"MARKER_TWO\") >= 1);\r\n\r\n    life2.kill_session().expect(\"cleanup kill\");\r\n}\r","startLine":721,"lineNumbers":[721,null,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]},"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":7451,"outputLines":155,"outputBytes":7451,"shownRange":{"start":769,"end":923},"nextOffset":924}}},"isError":false,"timestamp":1784195976688}}
{"type":"message","id":"022e1481","parentId":"d9e8c787","timestamp":"2026-07-16T09:59:53.518Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing ring buffer chunk handling**\n\n**Planning immediate parent send**"},{"type":"toolCall","id":"call_KtHn1WhULRi5Pje0nimY2wHz|fc_07547ad47f060e5d016a58ab91d884819a8a331cbd3bc22d66","name":"irc","arguments":{"i":"Reporting recovered stream failure","op":"send","to":"Main","message":"Refute incomplete part / strong root candidate: refresh does kill old brain-owned dispatcher+serve_attach workers, but new brain is designed to reconstruct them separately: run_brain heartbeat→spawn_net_consumers→spawn_dispatcher→run_dispatch_loop net_streams()→worker→peek_first_line(from_seq=0)→serve_attach→net_stream_subscribe+attach_as(same origin). BRAIN_RESUMED is only PTY cursors and intentionally no-subscribe. Likely failure is redispatch classification after ring rollover: NetHost StreamLog is ordinary bounded 4096 transport chunks and evicts oldest; dispatcher rediscovery peeks 'from 0' but gets retained floor. If original Attach Request rolled out (or floor begins mid-NDJSON), first retained record is Input/Resize/garbage; classify_first_line => Unknown, worker exits, and claimed set prevents retry. Detach+reattach creates fresh stream with Request at head, so instant recovery. Partial 4/5 follows stream history/activity: four older/high-input streams rolled opener; one retained it. Existing attach_survives_target_brain_restart_exactly_once manually calls serve_attach on a known stream and therefore masks redispatch/demux loss. No existing dispatcher-restart+rolled-opener test found.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":13112,"output":475,"cacheRead":98816,"cacheWrite":0,"totalTokens":112403,"reasoningTokens":160,"cost":{"input":0.06556000000000001,"output":0.01425,"cacheRead":0.049408,"cacheWrite":0,"total":0.129218}},"stopReason":"toolUse","timestamp":1784195976707,"responseId":"resp_07547ad47f060e5d016a58ab892ca0819a91ec4644d890a606","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab8d8bbc819a9ee5ea8dcdfa70e0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuRhyxsStCkz_9E13mTQufebR4lHRhgxYnBexFDIekVjTayznk-pRzg9hEgcL6sZHTdFstTo8n32zY-csn33lLBQMJor3U7ZzjJkGPlv5FtdM34bNlRxDR4HvIj8A5L0M15LAJUlMmm9CREu349MMDHl1JjuIZ4sLWulmm95fuSAzcNJJT9VGeWESvGbHclUEf1nq5SWESqME9yo12tvFt-5lkd-RcVTnuspjHRos-7jIk8QdtvgUumDy0iemmeKU1ZbNIzWbDBsAsdvq2tQfXiAcZrzDhnlF3rvLaoHVMeOjcatam56jGfGh5mUMJkUz5Y_eIekhCr5bW8K7op1bre0Bkunird4UwQWbnRbMtrQNQzdLBiJBw81l064_KjY_j3ih49j73xd8bLbWRAWKF_tN8jII5hEkmQ3dKDjuH9JsicEHPPt-MIH_emdct7R3QFkktSvKfQpRRwyF5Cwwd2pGp-pasUb3ulBYVCAPblqOQU6HxGyHLjj3ccOhZ4aM3AotMXib7FjAP5HZbft6KZK3uz-hueOeHtUEe-XLXW_5H7RDpnQoeDy8wKpEv0Nn69iQCmxAaFJXkky8au6D9tM-gCm6B_dSlk9-80xbjuh111iv1x0Bk_Lk8TH_4RZnd3lf1bnq5FVErsM9FkvtRH11ldA5qF3IPyGVfv_VxszNt-7eGAXcnYffF1nLSR7IUyStjRc3jh1xJ2x8hk--ldB7RrshtolZMansphItZxBFMQLCciqw_HQXDXWo_UNAHFscSGha0sNZqrs-atesudQ_wzmV5jRjXJZmBiedgACCoG4SaQ-tuBgfmbvZUUniyFvEqASknw47QuIsyHySDJyAob_xA_WHLmJ1wL7w1cLONw7hTG86Hz3rANR9Ul2II2c2_fhcgy8Mm48PNamJEKTt3JpeZ-ADyutVVENQuObIZOaYlwAl4xbrxLqdJVDCWivcGM4zBk_vYky6k3H9uu1wTSSS8rTPiGXJlYd5Lepxww-HtEOT2IGWt9PB7QDOzZOCBkuPuwBU2IycGR8essdLXHZnMDGgnMG468CuKcpPYdaLZEhIzRrurAM9Xywaye2HhGCfcFEhBWFrCK783lMjk-3S_tzqdCeIz93g9SdYSZKIK3km0dMa3uRHQ0Wvmf9VqRW2jkRFuK8BHSkAHSJ8pP7WJCLMaqtKz-gBjC8vGntYJaBez8BpSA7x7gEn7RyV86DFinTs8VXSBIIz3oFQs0op5_0d_hhIwe5WK7HwyyYzgXNWHiUDt8oCPqI1VNgolVodNRjo0Tx-AfS2po5N5KZIH-UBIX8fWlrQa2VsAvauSMk5PFnh6H_7HwoNv2bZOVpWuwm8rva7mD5EiZLRvnBqHZGCqKtB8SB3qKtmhWUbx1YHopT9bJ6qGrHNTGsLAlC0UWdwZbkH4UGZ15GS9YmV0eWXsOygm4inZ6DvqK_uJgXzBWLy_yKQJXK4au6Tmt8V7tyoQOTpejuOH48v_mJpiMyS7WLX1BjJQCe068_kGtdp-f3XN6HVQnvAqiVui-ldlyP_GopzBxnRWpF8-9ayQJ8TxYxM0boLtsS-atE9-w0UuwwSes_7OQva7JcU0kxPFWCbGonKCXobs1anEDQJRbfemOAN7Ni9LgnHsLCv8Yl3ivcST47tI-YrRDUPeWmGN5Mb9csriExowPqdPypC1lGqu3Giviy2Y_-nS3ezZN0vE_SKFtctVm0SAEdl0oO32Ocykb2RkzTPH--F8iFc-nyaJLgxb4H1yGh8jG5bab9rm6Cdzr1hpuzmHZjPhLNjCqtZxhBfec1mTxYFM_WAq4IQ5-LhBNQTmez5zYo04gHsTHrvMysW6yh8I9scutZYR-7zYOHWQZedNxurjBytAjmFj18naSJ6dZdrEHz-BQjmS3rh2qrmMqPKOK","summary":[{"type":"summary_text","text":"**Assessing ring buffer chunk handling**"},{"type":"summary_text","text":"**Planning immediate parent send**"}]},{"id":"fc_07547ad47f060e5d016a58ab91d884819a8a331cbd3bc22d66","type":"function_call","status":"completed","arguments":"{\"i\":\"Reporting recovered stream failure\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Refute incomplete part / strong root candidate: refresh does kill old brain-owned dispatcher+serve_attach workers, but new brain is designed to reconstruct them separately: run_brain heartbeat→spawn_net_consumers→spawn_dispatcher→run_dispatch_loop net_streams()→worker→peek_first_line(from_seq=0)→serve_attach→net_stream_subscribe+attach_as(same origin). BRAIN_RESUMED is only PTY cursors and intentionally no-subscribe. Likely failure is redispatch classification after ring rollover: NetHost StreamLog is ordinary bounded 4096 transport chunks and evicts oldest; dispatcher rediscovery peeks 'from 0' but gets retained floor. If original Attach Request rolled out (or floor begins mid-NDJSON), first retained record is Input/Resize/garbage; classify_first_line => Unknown, worker exits, and claimed set prevents retry. Detach+reattach creates fresh stream with Request at head, so instant recovery. Partial 4/5 follows stream history/activity: four older/high-input streams rolled opener; one retained it. Existing attach_survives_target_brain_restart_exactly_once manually calls serve_attach on a known stream and therefore masks redispatch/demux loss. No existing dispatcher-restart+rolled-opener test found.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_KtHn1WhULRi5Pje0nimY2wHz","name":"irc"}]},"duration":16799.987599999644,"ttft":4821.938499999233,"contextSnapshot":{"promptTokens":111928,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KtHn1WhULRi5Pje0nimY2wHz|fc_07547ad47f060e5d016a58ab91d884819a8a331cbd3bc22d66","toolName":"irc","startedAt":"2026-07-16T09:59:53.519Z","intent":"Reporting recovered stream failure"},"id":"79b7f41e","parentId":"022e1481","timestamp":"2026-07-16T09:59:53.519Z"}
{"type":"message","id":"5370c8de","parentId":"79b7f41e","timestamp":"2026-07-16T09:59:53.519Z","message":{"role":"toolResult","toolCallId":"call_KtHn1WhULRi5Pje0nimY2wHz|fc_07547ad47f060e5d016a58ab91d884819a8a331cbd3bc22d66","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"ReproHarness","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784195993519}}
{"type":"message","id":"a7805e82","parentId":"5370c8de","timestamp":"2026-07-16T10:00:05.630Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating test harness for deterministic loop**\n\n**Planning worker termination via brain drop**"},{"type":"toolCall","id":"call_WoziVwqrvamriti2dGgoVgzx|fc_07547ad47f060e5d016a58aba51650819a985bae12ea8a969e","name":"grep","arguments":{"i":"Finding injectable stream ring cap","pattern":"ring_cap|with_.*cap|NetShared::new|start_with|test.*ring","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":4393,"output":268,"cacheRead":108032,"cacheWrite":0,"totalTokens":112693,"reasoningTokens":183,"cost":{"input":0.021965000000000002,"output":0.00804,"cacheRead":0.054015999999999995,"cacheWrite":0,"total":0.084021}},"stopReason":"toolUse","timestamp":1784195993548,"responseId":"resp_07547ad47f060e5d016a58ab9a3d50819aa788a5ef7a0c8301","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ab9dbf64819a80b2bef528526d32","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuloVftbLt4VCu0u0-Fh2YINWeBTCB8mUiuQclatBMsrrsLOsEb4smHJ3x3xMP31c7jjDRV28sYfqQW_NAWP9QiEs_JNL1y9fIFnDxzHZcPVMz_wM717GGhKvLLAWWDyInevtdX3HC5TyX-PTS0jI2FGH10u0KG1jBRp4k5l4KR3maSYOphm149VxVJkejr9JSYX4KPQvEysnr4l9agLmXiHKUvvaGSWZH4xdq93Z_uFMXOgc-OVZ0L7SPSNBJNpo18AhiyfwL8qhUOym3EDUh-mcqG0eskuza_CNoueaKJ32XEDI344VZah8waM5jsWzjC-GoI1HbnXowVns6MYxVpuy6qfNRKmbtrB4gn7cyaJUoNrzeiF2JvPXrpqiwQt4iff_pmCVoIfh1LjcaYXbcCkTv4iW92MFMiuGZtwWq27slNtjPs_--q-9dcVY-v--MdWH9F0YaV2edzNHZx-rVfb0mEGZdRaZNx4yaLAPAAhSANiLonniGNBIXVHZ5nIFW-ffQthDBqCoVAqOlSXxhMdtE9moRmmzKmbkLcq67QHM1UUGhkHpz9k3FplyvYJtuBXuteDmCWTHUzO59l4ssmd1TzpJ6HiDmGFnpKEH5RAQtFfBjteQcCDjZgcSms-mzMmtTlUUe5co_Bhf9YbCIzaZRIBrgMJ0WcbGQPjgDwUs9p0f2q5nBlqWQOepYvFx3swHSg0skWLOHbaEv0jgo_e953jMlBUDq0jL_C30Pr1T5Gbt4mK1DRNmXXrXRngu7pv4L-ANLDEVTYxvZcwfDKWMxyFJ8eRCPq2cvr-Epl81L8Svy8SZkqI-o3X7zvnRBzRuh5vXE8ppCDB8T5haMkXXmL2SbeLrzh3G3Obha1zha5qUAhlJrw8jPZhxCsA254gsqq8Oen4TrClJKgYa3VD0ctdbCuD36J8sTmOyvz7KhPUnZz1SNzOLzTq9dd53ar1iJyKEKzMASfRE9pUwbjJ1P4qBvuidj5D39tgRKgGUkeomI6uNWUPK5m6gnRxSTFXde3VKcyUMFNLttbXXetqeG01j4z3gq5rGTlx_Lr01YZshEg6Uw6toS6LAwfjx0w1Wk2y7VsCR1nV2JrnHM-3I3fj8OIhBC57gtHGbL9g2xUzAo0BqM_rWbQbEaaK2HcjlQnJ_FWMVUZEllo4c8dGGeSK97p9PIEfyTrjQFCqUUwfBIrkm7vh6OOCVVNvANBUY8-OJ72EgV8h_2aNtflfhCeyxY812KFqI4pOAibOVybkf3sGWI8qy9uXuca4AGetKlp9lqb8u9-eqltz4Pv3O0X3fPFU38Qtd0KxmrfsMBefxgKqJ_GnTFNcGmQ35CTB-hRG0mO2FAmfrBWky5m7iDJLX9YbCUmWIqfhPfh5-0htsovHkSOeubld9JbF1UQmhCSeXMq9OLS9R8mq2vVoQESGfjf8lSsfcdwmyluVvxGe7D_qBdgqZfPBaJPPKrBsvMOw1ItxztiECl29iEo0I469h0c_dH5qUcAZikRZTde5DkKfA0Q5pHDTzvOKR_GpLv7M7aWaUl-w2Fkz-NRYIxjwVTHyMPWI2rHjEsEFsdJtLfwNjnGvl-h0Tnax1ZA-2sI7Eg_u0b1HMCJ4QaHmRWqykkj86vUtLe3klFuIIpdjbp6WvIBhQ9Co7hqslCCJU73lYzQkuQgbyWiZkpExbGcBetsyrTX2hxiDnuz44aQCDT30jEO8v17M-Ty6kUlmVMP5lF8m0jGNPhDKc3qM6_zeNw7sfXvzuYKxCHMrmm9W3xYVZZw4EqbN-37W27tCfBQYu05WIxXX1orhhmPfnAFurgaDw28reUzr2LgH4_bjg_GzTogDREQiGez-PEbIfz8qOIBA3VD7QoLo3WL9-Uhl4JXULcy6sHL46GBqd2m-_gRqmOOOBRzVe14hLUrYJ1193K32RcUcSaxy-vlkLvvMDM47pb1vnAhaLp9MX5GC_vSql5wwHVp1qq0NVvPDmLfBUpntooOWy6LyfVuGy0Uxee1_us5yfhqgYnzFQpq07o0boVjVIa5byMvG-gEASZc8jC3KNYVzQynL_Tv5AWGmWAXIfIlbWyLnyJzb7K1ycmoclUTQcnNdQo6K_hAS0lpmxKuqhiNYOLvjHjt0H_gFmpwJ99ZJdBqUA62TEpXByzF_hZqTFYtQEqdBc2Crt6cVluubxfvi2Y2ncXV5A==","summary":[{"type":"summary_text","text":"**Investigating test harness for deterministic loop**"},{"type":"summary_text","text":"**Planning worker termination via brain drop**"}]},{"id":"fc_07547ad47f060e5d016a58aba51650819a985bae12ea8a969e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding injectable stream ring cap\",\"pattern\":\"ring_cap|with_.*cap|NetShared::new|start_with|test.*ring\",\"path\":\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_WoziVwqrvamriti2dGgoVgzx","name":"grep"}]},"duration":12082.0736999996,"ttft":4175.523099999875,"contextSnapshot":{"promptTokens":112425,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WoziVwqrvamriti2dGgoVgzx|fc_07547ad47f060e5d016a58aba51650819a985bae12ea8a969e","toolName":"grep","startedAt":"2026-07-16T10:00:05.630Z","args":{"path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests"},"intent":"Finding injectable stream ring cap"},"id":"ec6616f6","parentId":"a7805e82","timestamp":"2026-07-16T10:00:05.630Z"}
{"type":"message","id":"93af011a","parentId":"ec6616f6","timestamp":"2026-07-16T10:00:05.657Z","message":{"role":"toolResult","toolCallId":"call_WoziVwqrvamriti2dGgoVgzx|fc_07547ad47f060e5d016a58aba51650819a985bae12ea8a969e","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\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...\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 838|        let endpoint = Arc::new(endpoint);\n*839|        let shared = Arc::new(NetShared::new());\n 840|        let local_pub = cfg.identity.public_key();\n 841|        let pair_rate = Arc::new(tokio::sync::Mutex::new(\n 842|            spt_net::net::pairing::ratelimit::PairingRateLimiter::new(),\n\n## tests/\n### attach.rs\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### conn_blackhole_lifecycle.rs\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 67|//! this test binary compiles to zero tests (no fakery).\n 68|//!\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 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### rosterprop.rs\n 45|/// `production_roster_exchange` minus the file/peeraddrs plumbing.\n*46|fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {\n 47|    let prov_store = Arc::clone(&store);\n 48|    let sink_store = Arc::clone(&store);\n 49|    let status_store = Arc::clone(&store);"}],"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":9,"fileCount":6,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/presence.rs","crates/spt-daemon/tests/resume.rs","crates/spt-daemon/tests/rosterprop.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":4},{"path":"crates/spt-daemon/tests/attach.rs","count":1},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":1},{"path":"crates/spt-daemon/tests/presence.rs","count":1},{"path":"crates/spt-daemon/tests/resume.rs","count":1},{"path":"crates/spt-daemon/tests/rosterprop.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\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    │...\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 838│        let endpoint = Arc::new(endpoint);\n*839│        let shared = Arc::new(NetShared::new());\n 840│        let local_pub = cfg.identity.public_key();\n 841│        let pair_rate = Arc::new(tokio::sync::Mutex::new(\n 842│            spt_net::net::pairing::ratelimit::PairingRateLimiter::new(),\n\n## tests/\n### attach.rs\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### conn_blackhole_lifecycle.rs\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 67│//! this test binary compiles to zero tests (no fakery).\n 68│//!\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 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### rosterprop.rs\n 45│/// `production_roster_exchange` minus the file/peeraddrs plumbing.\n*46│fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {\n 47│    let prov_store = Arc::clone(&store);\n 48│    let sink_store = Arc::clone(&store);\n 49│    let status_store = Arc::clone(&store);"},"isError":false,"timestamp":1784196005656}}
{"type":"message","id":"67d7b063","parentId":"93af011a","timestamp":"2026-07-16T10:00:25.457Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating broker constructor behavior**"},{"type":"toolCall","id":"call_FO548nVzxDdI1cGpD3YDyj10|fc_07547ad47f060e5d016a58abb96570819aa6f843de918f2c69","name":"grep","arguments":{"i":"Checking net cap constructor semantics","pattern":"bind_in_with_net_cap|fn start|with_net_cap","path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4400,"output":100,"cacheRead":109056,"cacheWrite":0,"totalTokens":113556,"reasoningTokens":19,"cost":{"input":0.022000000000000002,"output":0.003,"cacheRead":0.054528,"cacheWrite":0,"total":0.079528}},"stopReason":"toolUse","timestamp":1784196005704,"responseId":"resp_07547ad47f060e5d016a58aba62708819abe427b8bb8988fcc","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58aba92e9c819a81dedeae2e4f5a6e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKu58atuYa72sFVu7dhLfnqDsu2ti2ofiK5HkeOSQYh4oyuf5EoXzwCQyojXzzFQBcwyoQSPIUFoqF8Bbnw9tumJ4N_2BEMIL3d5---mKeM2fu3kWRp8hCKgXqnig8I712jm0rpNhzW16yIQYViJWmMt9cWKJ7NlU5lSmtWCv8XI04CuvOtB7cHeCUjJb3EPKcxU_x_hbYMMYcQ1fENSNF1KtOfxWegbesBEGEyHZj1MLfxwhwZZGMMVAHEi88Voiw6RW0KPY3EskiMlHs7c64LQAp3IdQV5Zyb9tvXtTmFU994b7h-t7SJVGnH41fTxfARiN8SxmhIEy20nKBBZRA_Ko02Vu6lv41pxcMVK9DIzR8WF_UUXEc5CEdRRtGSlbK-888MExAf3jSIloUvJ8YJ00hstkOYOoOXhvm_Giuw9DS1kXA3Lq0IXn95GKCTW9ReA3AOw6RIvxmh6tTxfIoSrB01d36fz-ltH5qU81q0ews0sPGlHkr2uOGgqClO4RvW8Vs3_Q7_jXmdryMpvaolgv62uDLr81ROhRnscDNlk3qPz4qvO6VXQsRFuhMPvt9GEcId0-xj4DLV_QgpLH-ynEbtxkXJxMgMT6Nz8Quoi1zVfXqHmpdhv6v8bEXc92iKfJDCjC_K-Df_EX9UxsKf4EwMTi93Tym0DTfObtmhG-gWpby_QfpviSHgC45ci0URxHEYvH2FX2ijZRyhtbEJKIv_2AukRrE_z8NT8yNJw2J9CRy32S5HCopqqHPPG6TMdNxG59IK6ACW8u9YFSDED-n5dEOdYOyY_TY1A3_mNoT7g5VrG2OWJTwnKXU0t9CTz9UUjT4zTZeBGBiKls_cjLd-A2tUNBf4WAMSLQtRed2xA3ohVwRXLaezajS0ppQnwzhNZiC6qLKl4IV4PYAHAHWsf2ZLFWr-ELXN0Wi-CcynuiBH7mUv34qO0LqKj5bRSKL_KzHRhYBirJG6Vo2Yw-nIKxYSrH4oS75uAspZD0wDANXxAqqJuQalzIZTmk8mZp-jIM8wjXSvA_92igGe6YA==","summary":[{"type":"summary_text","text":"**Investigating broker constructor behavior**"}]},{"id":"fc_07547ad47f060e5d016a58abb96570819aa6f843de918f2c69","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking net cap constructor semantics\",\"pattern\":\"bind_in_with_net_cap|fn start|with_net_cap\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_FO548nVzxDdI1cGpD3YDyj10","name":"grep"}]},"duration":19751.933800000697,"ttft":3481.9605000000447,"contextSnapshot":{"promptTokens":113456,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FO548nVzxDdI1cGpD3YDyj10|fc_07547ad47f060e5d016a58abb96570819aa6f843de918f2c69","toolName":"grep","startedAt":"2026-07-16T10:00:25.458Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs"},"intent":"Checking net cap constructor semantics"},"id":"3d75fd2c","parentId":"67d7b063","timestamp":"2026-07-16T10:00:25.458Z"}
{"type":"message","id":"4489b9eb","parentId":"3d75fd2c","timestamp":"2026-07-16T10:00:25.463Z","message":{"role":"toolResult","toolCallId":"call_FO548nVzxDdI1cGpD3YDyj10|fc_07547ad47f060e5d016a58abb96570819aa6f843de918f2c69","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 2668|    ) -> io::Result<Arc<Self>> {\n*2669|        Self::bind_in_with_net_cap(name, journal_path, net, DEFAULT_LOG_CHUNKS)\n 2670|    }\n 2671|\n 2672|    /// [`bind_in_with_net`](Self::bind_in_with_net) with an explicit output-log\n...\n 2677|    /// the evicted tail — never gap-erroring the live stream).\n*2678|    pub fn bind_in_with_net_cap(\n 2679|        name: &str,\n 2680|        journal_path: std::path::PathBuf,\n 2681|        net: Option<NetHost>,\n## nethost.rs\n 814|    // [impl->REQ-SUBNET-2]\n*815|    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {\n 816|        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the\n 817|        // \"no network reached\" promise — the ceremony clock's lazy NTP query\n 818|        // must honor it (tests must never probe a real NTP server). Real"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":2,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":2},{"path":"crates/spt-daemon/src/nethost.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n 2668│    ) -> io::Result<Arc<Self>> {\n*2669│        Self::bind_in_with_net_cap(name, journal_path, net, DEFAULT_LOG_CHUNKS)\n 2670│    }\n 2671│\n 2672│    /// [`bind_in_with_net`](Self::bind_in_with_net) with an explicit output-log\n     │...\n 2677│    /// the evicted tail — never gap-erroring the live stream).\n*2678│    pub fn bind_in_with_net_cap(\n 2679│        name: &str,\n 2680│        journal_path: std::path::PathBuf,\n 2681│        net: Option<NetHost>,\n## nethost.rs\n 814│    // [impl->REQ-SUBNET-2]\n*815│    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {\n 816│        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the\n 817│        // \"no network reached\" promise — the ceremony clock's lazy NTP query\n 818│        // must honor it (tests must never probe a real NTP server). Real"},"isError":false,"timestamp":1784196025462}}
{"type":"message","id":"e957a3d7","parentId":"4489b9eb","timestamp":"2026-07-16T10:00:53.573Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning stream cap test constructor**\n\n**Evaluating test stream ring cap approaches**\n\n**Designing deterministic dispatch recovery test**"},{"type":"thinking","thinking":"**Investigating net_stream_drain usage**"},{"type":"toolCall","id":"call_qR5JwAtAthW4y7pqJ8ZfAZcN|fc_07547ad47f060e5d016a58abd0b998819a9954e9b7918533b3","name":"grep","arguments":{"i":"Finding stream drain API access","pattern":"net_stream_drain|KIND_NET_STREAM_DRAIN|drain_stream","path":"crates/spt-daemon/src;crates/spt-daemon/tests","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3752,"output":695,"cacheRead":110080,"cacheWrite":0,"totalTokens":114527,"reasoningTokens":618,"cost":{"input":0.018760000000000002,"output":0.02085,"cacheRead":0.05504,"cacheWrite":0,"total":0.09465000000000001}},"stopReason":"toolUse","timestamp":1784196025496,"responseId":"resp_07547ad47f060e5d016a58abba2048819a979f01ded2a6d653","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58abbd3b9c819a991dc8aa2f6d0c63","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvNzyG762TKNRO3kLMABnHBAAANvDMLejRwDEchR57eJlwqxtmOPrNl52d6hA3R-W_y5RdIvBw50L_bWqG_RxnMrKim5r-oB8oQ3CQCQ6O6s36n_gew17M79b3jAr-I0-BM9oAYm8BX0OduWDnlV97765q_cdT6U2RJsAW8jbPCiRnaNMsVGOBUdWienwrOSGEavN7vEyWhTB2ZGhpugoCYd60gziwbHzyDEj6qTQIgj92aiFTWJ9cOV7lCqKbyZfJ9B4SNzFRp2VxlaX6neVK9r1e02StLcEIdz2O5TGj84BWwBOnFrbR8G7d7QqgFsF6-YP3ECtSO8IYtsnUVGvu6yge83sxbZlZSJcaS73HUCSE0kUTPwJbnxiZTgzaJgfGg9M4Rf8byFIoGuQg-0tL7Gh4i7ml4g1uZ6NEuKI6EykVgh4ynVslnQUaz5lNrERcBZIUU0a6_GIrkAbEJWv1I9a4XV58aQsHzNaAVPNlimH57yvE84CR8bx3DRrhZFreF2WeNhGG6Whlq8IMIvE-pEwFRdU8pFzReqrmOFYK250Jw-5F6PiQ0AxxO0ft4Ro_ucDJ7g44zBOT-TrU10CqlvMsWYwgEH24SzJAICSusnZVb2pQVE6ZxGK_THnTLDQwLYqEZTS2fO7QeXTDy7zF0dRqdUbw-YRGCgz4933cjOFzUdmL-ynbuLscg7y0J3sJ6ZLPk0QbSUA45wzNX2WB3jYEbjO8FJxV2vYHaBZH9N3JEAxPoIuPrvE57F9d8HA5LvdOkxOiuhxHV4zpMDRPn86Yke4UAaX3Ln8kL0goftgG6OZqPgNgfLU8ZzCo0EZidHlJXEufD-nabqo_KDIut5eJgDJ-ObvzCTRrCmC1An1plbs1UI8FaSUIRkC_JZ-N_o13CEuzC5FvGgkobTkZsrDDFMvmJKolFBwoLb9hlHIWfjkrBUjhyYOx07xDuW2cZYHdVWTVf7jxtP7_v5c77Lk3OxIHtFi4xQ3M2IeqxdGZSi58Nm3DQuxGQV2BnWxn60NdRuQii6d95keHJIkqo-OvMe16-GYg3VSMbWxinBPNIrVJiB89BcxlTO2QpcjQ1NPTtfrfWeHsPBUd4BsZWupqbsUdmb8jsCQTIum8ds5eEUgrawepesIhsIQ0HStmZWYosmZY2Sun9hcAhnJuC8oQC0cp1H2HurD_dNwVYPP4kXD40ENeror95phBH0KZsbz9eSKMPLC5d3cGqvs-XJtN84iDETeLw5uvDmhvt77tRH3MbxbdZ8C0-jp80olRGQk3ZcUlnivuHlG_d7NeM5009wdlOm9kJBrgcUlFYZM8Ns_Mb-OxDTgXkPU8n8nxN1VaZmwgUM6MbWnh6QSMkXTGXni0OinxzpAPUQvj08-aaYlWOffh-SGwco7Tp_oNOH56F6p-mTCfRm1zy3qckDwpem5it-UthhVlMu1ynOyfxlyK4biXfuGlnwqF5jHa6V5ML2SK6u0R1Yq10-nV-BbNsx6TNb9EXPIEiNBRh2Q5Mbs28XcHpDdvPMRoh1QCj2amNmw0I3J8g0Mv1X3qncemyoMvLIj9WvhazP8EuctytJ83Sf8xj1cxWegT7Cv9OCEk7-ADzfU8SLdNI1m8MlxT4cSnKqFrhgZHNiFaprXZUsaVkndTx4oEw2ofBlbsblAFrBlHBrvEbmv4sz8XOLTChePtDaMX2-1nBAbmduDHx_gJvZvPjCGx4WicK3_rHdRkxDMv5vhXiPGtMD1cprxj2iQ0mzMD34qT8M5HZf6_83-Zp8lwdzhCxqjjpzwZr9G2m3gQoCEdwal88spGhKcTlBpep0p4SyzKes4vzolsjezIMRoGDfE2sIUXYcxJ4TQnsOTmGGLwKJit9tF_xLa8CtzLZX7bruLcKJNaq-Ta1a5fHHC-owHlQ6L8KUonYAdtjEvqSjvWTFWn5hhnFFzN_W28yf01NRqDZTZ8ENEXZ8rOxXP-3rgfjToQyScI_nyfoTbEj7Ay_CarDV8yecUtx_k0EpeTLyx_Fd9cPw0w7apCqz2tXXsDL6KnRwUmSUnYPvHsxiAA8ROoGhLtujLJAZFZqhEuDvTVufjFyp88YyVL_IIWfmlTIUrl2ybSXtax_As2fvS2Voji56IbqRjsqrtJcgwfNlOE1BK1VKSNq1m6rnSudrpCUvxRFjYFXMBk2azGIXKgYxSjFv7NHf-b8YUiGBkPUpB8AuybYKYgaYqvJgAy-1d9eNVCfuGKXmrHxyNTtkyLNGXaj6cD7jH4DjL8Cd5KqucP31eZDtGS5XsGDORTyWA0TPwzDi1uMJR7uLeGqYVooiAhKxGQQX6CyTXBKe2MXHBMEWHvDM-2wlWDMFa5wouL7g2hMUCe07sV8WQY2ab1uYdj35TN2DZTvhzmEBmqhWkuWGWNtvdyeSnjE1Q6W2ziDQn-rSzfQSIdX_5zmRPdTloy5NeYkYfHNqih-g6JpEJksZm2xO09epou_-vgezeIrnrpieYv1H-Cb3r9QVMcTvlFr_Z3e4tssQMWjLXpTKJSDmhIb3EsNnUZapx7sI-iX9Jl0krWfpSEsEYcuaMVdxiGiPF1A4FQNpDH-PGvDyXu0QlYZgwjUgGtuJTtFS-D3lS8E4jJcBc6feIm2p9KyhjN8quBR-6KVPROkgyVPebqd4WYq1m5oNFJ2OItHqfBNhTm1XpSFArSYq82FQLfp4RbN_JNHVOr6DXUPkLIsrpt0e3Ge7qnfb1n1yZGqdCLKqToGd7eQE60r_itY15JY-X3t9TCcwL1aB8kpkZ6WzloREyyfwJCuni1L9uWB2CtEl8z207JskHjdSnhbIrlZoRP0SNt1CMxi92zyGhh21HZU4pFWi-oBWUVh8ENEOFmq0-Xjsnqq-XjgO9JGbSQgVD5TCBpRFiKhaXBg7Mf5hlzHQpBS6Pf9Gq94obJhtXflD-v2zUpOT5iC5yGDb-QX1VvvQPbMvQD20hZ_SMZXn5iouih2hsEBNAlGandAEerUqdBQ-09iq0MZbGTEAlKyzmyYu4N_mYkBjtK3E7tTcSfqJk52yHaXKrcWa1IlkKCDGS0p3AKiayaSPY2hzDxX6B8hKJp-zRYAPT1PuIjslJrQiuprMx1__dWRbaCaOIEoR8dIzzm4QlHuMBqNF_yJKw4KRRotiUcAre-AJ3hKUFt5pXmqRwbZWr_DJsN9dQwj_YhdGKU-xGCTyFI9NtLKqvP5FB57mDFgAtluXe6CmXj1CMHuucbLoeC8m1bWqNM24CxlxblqqMvtBxFi_r-cOKplWUvkqsuWidO-uopPWdaQU82SkyTRDMmbGMXxj_wzayiDH0ORaK-XkQ8UCeES4yOmvRl6ChceqknN97-WS2lQoko5DdxBdeDD94M8leYIjR0XQU0XjA-vKq88ag1FN6QiP8nl9kAwnZ5kMKKLzYtVWzP1vU2BY-7wMz1ZroAmtnKxMe9e77nlWP3v3IOjmoWMegW6THpdvWSZ2neck_JVxDotSBWHE9zZ0b71eXRqvU9CTpHeAyvRUxOQ9RkS8e058CE8GGum8qcg6h1BvLmj742sHpCS59JTp2rETy1WoWe3F4ZMIG6fCVWOLigSirVXl53-7wnUd-Foy9eEtBWM3hHvgeyxluZ0_gWfrv2y--k-iZSbE4tu4AD1N25AtTU61qmdHQ44lqUprhcfktvQS42nRzNw9KG-lLORr4_lLKV1SLLIRfRoaL70SCXRa79SL4HnpenlERJ5jCt94voEt4Xx_y5cIPJkLI0IHpcXYudEefzQfnZxbDDa-P98n4DdCmSrA2PhPU_cEss_f66NsKVXdXPnWiYXzVS_lKr44vzH-wpSw3E-8E6_xik1Pac7WVcTtnFBYPS7YPK3HZMGqZj8cHlY4V9lXVYr8XcEmyJOXwpIInHkI5N3R98wjjRS3Vv5xC7M5SPp5Lv-dxY4rN4u7Nv4ghdhJlQC7QiOsckdvZOwlt4FBwcjn8_q-vKQsamPTNgFXlKZ4XxNYkzmPA7OxjjuKMUTtRq6vwo95Lw8xRkY2pa5ofGMHZdSRpjvbZu1hvxgjucKfS2U0Q8eQcQvNEuG0-mVfSpRHAPUjhu41uzHDRgKQnq2JVTRXcCQHO0xNlHlS7N-PM6B2Vh9QDgCOi54z2orqTXyXlbHYP_2AL4hvDkd9Wgz4WEc5l-jm9VEaJytXUeeP3Z11Yxqc7oX6N7y84SrXvrmlN82Jj21my-zHpvYYtjIPZn-fw6Gq8e4FdaqQZ4foKnpZ84eDIt2YovBkNG5ZqK_mZtQ7gG439zaLKt41aKDq3orLSZaIPI4ErLfYVz45z6I4QC9IPluc-k=","summary":[{"type":"summary_text","text":"**Planning stream cap test constructor**"},{"type":"summary_text","text":"**Evaluating test stream ring cap approaches**"},{"type":"summary_text","text":"**Designing deterministic dispatch recovery test**"}]},{"id":"rs_07547ad47f060e5d016a58abcd4598819aa48225023129cf3a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvQicmJb_UN7ondI4Foj2TtgRoeU-OzNF2aJT1VnTfqykgBX0anuGtmto8o5r7VhA8hddbfVoiTqE-umPC1EMwmZhE6vkQrsZ7gjJs_ww5KBd1cDj7lJvroU3-YolbbHqxb0los9lbvIzncwFRlF2A9IN8OBN1E6P-pAfw_8wKEB8e3DdAeC3soFXHUhTDKVmVJNxCJuDSGGYTjDrc0lKgGpRU9YkAAP90OS3KASqiCMiIBw_D6ALTGYbrzSbOQm0048MuW86CiERaLo5o93zGkWUeCiOJp-YjDDnflWqMwVOOZn_eg1W_GVKf6QVXy3BU7CPctSyIpWNfBj05J1nSQoVRayd67Z9yV9T1TitE277rLBdYp2DkdpSSKHkt6GqXoU_2Y3R7qkV8c-H5As3EOWozzCVin9hEIiEsaLbgEAMA7sr2_ZQrXjwxXGlqKrrkGyUGP4hurgergh25YKBVfqWNomOEwI2ipFt9fq_XGzUWw4LmkzLLW1t40qG0JY2wROEohkBgfw5gfaaTzORJ_EqJ4bDwTcwnSDIE_nNKoDi-jA0nASKXZ_3LTh30qXDyBFW297Znl-wvHA7tFvqxcmNJXPTuxEnDzmtAzWKdVJXhpkT6UC6NkJYvShQ6nUvjG08PYbJQkZ6Pt_y3pag2fl-B0MVXio53f3JdD0xS9-5s432gxEUB3mpV91h6Tyns-Tpnfr5PEcK6pTny5YV7b9LKYcpHHDwCaJPVOLsXqMnq-pyWNOJYYWYzMFJsGiCzm3qKmJsHPxutzIY3-zkxjTBUY7vFf20jbbiWeJk3GEEHD0C6g6GvliMMJDH7JdSSrEwQHfEY-2AqNAiws2hSng5W5OiEiIxeZYN49PtT1g9UOVQ-0rf0MviOtDfPH7eOZX1pc_CpY9RGkNGDFtuavOkvDjwcDqy2zAXE8xLI1DFHbwCWHjY__WSfyVMykUciQqLox5JjJmN-noi-mlR19AK5XIZbxbtpUc_LDS19t039R4PXXtIrnuVfMyu_7ZjzKaeTVhqr3acVOaiaCsK2h78LovtnhrIWdVnfoNMF1IL65t2dy-nitOCTK5kODLH4MMC5VDK6YOtA-BHy4NYeiMUMXNyr9WWB_5trdl3o9x2fohF0K-QnQFJSKuiTDjbOSDMHHuUsATt3XU6dDsaQsF4_igxND-nxM1xPBGMvWvyyAIK1Who59XvEvglOQxemo-2gLoRRRjG8D5qr1dy2BUBEj5Cur6W2COVdXxvfz_SoSqULwPJCdDCqJsgW5W484YLjJVW43er9h-y-qa2vkenqCEYywCo-IvNcM0_GOky0p69dO9VkVJ8Z4Pk-h2cjj-ikgWG4-cQ7_pBwvnmtRk-LN9FHHkZRvjgXgPjoehbaio6KiZ4Ci_bkIEVl7Y-7nANNDNz_ZXX52GTinS0JyCLhroKa088717XGj51In_C-7lS2vLFtx6aKSvGgFfb5wWaGApUI4FA3XbUyxqjahJY9dS-hj2GxlMgj5Cwc0gFo=","summary":[{"type":"summary_text","text":"**Investigating net_stream_drain usage**"}]},{"id":"fc_07547ad47f060e5d016a58abd0b998819a9954e9b7918533b3","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding stream drain API access\",\"pattern\":\"net_stream_drain|KIND_NET_STREAM_DRAIN|drain_stream\",\"path\":\"crates/spt-daemon/src;crates/spt-daemon/tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_qR5JwAtAthW4y7pqJ8ZfAZcN","name":"grep"}]},"duration":28052.547899999656,"ttft":3716.07600000035,"contextSnapshot":{"promptTokens":113832,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qR5JwAtAthW4y7pqJ8ZfAZcN|fc_07547ad47f060e5d016a58abd0b998819a9954e9b7918533b3","toolName":"grep","startedAt":"2026-07-16T10:00:53.573Z","args":{"path":"crates/spt-daemon/src;crates/spt-daemon/tests"},"intent":"Finding stream drain API access"},"id":"a787dc7f","parentId":"e957a3d7","timestamp":"2026-07-16T10:00:53.573Z"}
{"type":"message","id":"b31ebed4","parentId":"a787dc7f","timestamp":"2026-07-16T10:00:53.585Z","message":{"role":"toolResult","toolCallId":"call_qR5JwAtAthW4y7pqJ8ZfAZcN|fc_07547ad47f060e5d016a58abd0b998819a9954e9b7918533b3","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## nethost.rs\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...\n 1825|        for _ in 0..2000 {\n*1826|            if let Some((mut bytes, fin)) = host.drain_stream(shell) {\n 1827|                got.append(&mut bytes);\n 1828|                if fin {\n 1829|                    finished = true;\n## tunnelhub.rs\n 356|                    .net()\n*357|                    .and_then(|h| h.drain_stream(req.stream_id))\n 358|                    .unwrap_or_default();\n 359|                let res = TunnelRecvResult {\n 360|                    data_b64: encode_bytes(&bytes),"}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":2,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/tunnelhub.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":2},{"path":"crates/spt-daemon/src/tunnelhub.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## nethost.rs\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     │...\n 1825│        for _ in 0..2000 {\n*1826│            if let Some((mut bytes, fin)) = host.drain_stream(shell) {\n 1827│                got.append(&mut bytes);\n 1828│                if fin {\n 1829│                    finished = true;\n## tunnelhub.rs\n 356│                    .net()\n*357│                    .and_then(|h| h.drain_stream(req.stream_id))\n 358│                    .unwrap_or_default();\n 359│                let res = TunnelRecvResult {\n 360│                    data_b64: encode_bytes(&bytes),"},"isError":false,"timestamp":1784196053585}}
{"type":"message","id":"e2bbf395","parentId":"b31ebed4","timestamp":"2026-07-16T10:01:31.272Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Testing manual serve attach and stream draining**\n\n**Planning real daemon regression setup**\n\n**Designing session spawning via raw broker controller**"},{"type":"thinking","thinking":"**Designing fast ring roll with loopback streams**\n\n**Planning deterministic and smoke tests for stream rollover**\n\n**Noting dispatcher restart impact on stream draining**"},{"type":"thinking","thinking":"**Analyzing brain restart stream handling**\n\n**Planning deterministic stream stall test**"},{"type":"toolCall","id":"call_hb6if8NbGxTBZoUDyuP7M4cv|fc_07547ad47f060e5d016a58abfa229c819abf25e52c20ffc304","name":"grep","arguments":{"i":"Checking broker net accessor","pattern":"pub fn net\\(|fn net\\(","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":4708,"output":1377,"cacheRead":110080,"cacheWrite":0,"totalTokens":116165,"reasoningTokens":1313,"cost":{"input":0.023540000000000002,"output":0.04131,"cacheRead":0.05504,"cacheWrite":0,"total":0.11989}},"stopReason":"toolUse","timestamp":1784196053587,"responseId":"resp_07547ad47f060e5d016a58abd602f8819a80439aea25a7582e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58abd9c568819a9b27283d2663840f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvpE-rpV_47Zh88nsbSe1jIsRIJZ_9q15chLaeVkgy6Y6koAsJCHBQLVEKU56VeJF5V8GXfYYPAkNyiTxS3UauFYiO5u-Bp6NepMGDsXxvnANeLzlRjgZgY-o28kdmp9y_C9uP69pVRFHn6ZPSayJ_HnTsdT-McPZiH69bc2whexZ7j0-ZT1RiZ5up02hQyRcblnlHnGrLmX9XmFV0izaruq5JFG3TXTZtJg1LyZKLfRGCZmGZd6Cpv605j2gPxrlb0lbKDnfThP2awuBe_8OdSelkAoiXONY51cMklZ9NBvbikdBoTWTrtPIxj1K7wjhmkfchif7GkocU6U170DnBF9a_4oZYx_C_RNak06LPCxhupcc69nf4RARnQPw_jXsBa4pz43zub0FRdbUA7Es9STzjGkTFNciYOd3g6T4KYP8sc1iZWArkAoDz8L0MYdNCX7FIpNtth2pGjOE0kNiJQ0vf6QKKQmHdZig0eXleDfaqSPYwSrtSxqKPJ0ZXv31BNP0ScRLhq2waSTwm5CncTmb-9rsaMjKAAIn233wP6JY12oDWstuf-ODKord8O7fUsSlAKYt2-N3Zgo5oZSHo3hMFHW_4v_qdYZ42UnBnnejY8Tq_-0etdTu_vV_bfY-Gv_B4Ztl25pb5ordxUv4oUkKsp7-jDWWXud2uoxraMG5Bl_I4YoZMo4qtSPPaLwu-QGKPehsbOqjBVbVyxsvjpwZfW7po9TFykAVDVnHmCBJy6EuStKUh7d4dE2WuSZcjO-LNkcU-VqXDv4ljEZkMcxsdxXm_HLTo1XjsQXWvAuHdzchNWHvwtnbfTLcc9AeFhTlrsG4FtSHuTE5x69kyX31GgxzS-GV_xZCHMYTLKwYWlTC87CPSQNl8qOHgDxrrRk6zG9aZmGp2a7_iZeHrR9B1ao109JzoAscyoZq3dqwOes9GWbdkpf2tlNEGJ-p4jU5nniuZrVVN4kUauiPkvr2aETYqoISxjhXvLXZ_1GxmKFKXy9yqYY7Fj7zj7QcY8zp-t0eKMlXHtlFqy4268FWKQzLvkKJutGj3lgYrwzD1ov-J5JGZEWcwiyUNTZPidJZeyuSxQhP-77oik9vDvw2v2HgTVAMWuowyrYfrah_fd5C44NTe8h5qj-ma72y3S0goUUh11--8GFAnYw0kJ0Pqm9Usqox3reNUvhIWJ8EYUuV8kZOyKzIK3QwJOoqoVNl4fwVKYA6BgzarAJTHBGzw6nkDNRoU88kbCvFtPgicJK8DYk3aJpV86Q5Va5urgHI06_DvDtXwn2sapQ4zaQmlYHVQkyrC1xtZLrbj7yLNJaxQwr6N8c6c_3whSsB2mTu2guOnRbVjEb3OIR2jrrGGvQvyWaWdEV0gLbbBRfEZqaHpKTJPzLZhxfnr5B-11IIp5moxLoOtyE7p5TyHd50gx32vm6YrC54kMD7xllhypkuTAPcZDN6ffpx2kEt9oqCGQJFUM692fcJgo3Uwv7mvWJoZzqmz4cNAvoWK4NmLiqwQURVEs7fvTZoWDfB13cJXiBDPAIqbt5vTcu7wAV5ua9t4mtjNbme3CYMI8to3k0F-YrT4RczRpwzcsHNqt9bWagLQ9HYdb3Uv5sN-g9UFQSbD3xIiAzaTtyamuqGudoDBpFW-rWAXbgjft2jyzezuc8MFwaWfDCFuZIUI88mhaAfREKrnEsj_isoAOdFDi0jl4MpCrGNPPdA2oJ4WN2M3Rmz8LphhZbeQrFIAVYlJL4uIXhtH8GeDOKl5kK-XF57QgDrx5MywrYHjCpQ9mS7ZIPcFLumcj7fMnHobPhzME3-Q9qNMSH04dn3iAh6KBLkJN2mx3qiORehopjUQH9zUre4OHKijZmSyCmbgqv9h88r9x___PmH6_9mBtNWlwWc2wD56skPChVWzxvRUNl09y4546nuPb2P8j3XoaId1U_Fz0B3Z-ZWPUrqElS9NOezuYiqd0cvIgR-Nb1NA_TvxP8mBwWsZLeVjOFKmKXKitIfaHseI_NB67-Jz5ddwyubrfV0_r0f94ZEsGdHXF0_521jhYsoK6bkNBXk4cYHLpNTLpq49N46bj-bsitk4tJ3o1YgZQCzXME4jsN_2zo5mbaP6PNDbie8goevgTKN48PL1I-n7LZIcP4MNIYjKMsnsWDX_bP4BfNvzVHVSJsd5WWCRj_-EuHwW-qRwGenU09oBJtUT3N1KknBhIbSsooGitP3Kkv-e_Im7RIk76i-kubXwf2U0A-hIyfZ8Z9okYiZLz_0zO9LvMaPU-wrv9UULMBn-spQQ5HcY5uEh5R07FaT2i5sftmOAbeMGREwDC9VIm3vGZotBecxmrbInuWcm3lQhgQEFlziXbxt8Efd_co-UafVH_qGYu3pqa5ucx90eVbkfqygGED7qGp1ek5CcufN8bQLwFcbEKoBAao_Ka00-obby7Jj_2SWvadBLtJnpSlUkH_r-hN_sXIhRQDXUE6EdL598jYSg4hxzzS3EliDWy727XdIEo4wZXy_OQX_NAXUJrUUWWK8DUvAP_l4pVeDiyf9hybEPdbUxYidQAFx5gcGSe-9gm2y__crmwYvkeh3qeZQMqAXMHJNzciONdZYr4FcSusSkxVCOEJ2bGDBPedcCUDTWbmzYjW-uCBIGEIV3X2BmXyb2Spehs-P9BiJYi2QSLUJ_68jLzTgbnJQgqHwBKPOm8PU33TPaJg4X4Orvh47VtFHkkWPDoYB7lNBGciryaViTMracrBW288uoiAsMVfSvYExZ-GjHoUzw4d4bkQlFaS4WOGc4UUeLuPoAJeJAiQ76LduX1mh-BSZID0_BMvhu6WkVeVAeoO2F8QvquVg8U_ZbE1322J31Apg2KMa6rggg9uLR5w9eYs477jFp1ZZUn3WZYJZxtvbgPWYJTJBQ21teNKss-5xYTDmxBsW1Bc76vMMzfWg4_ZbR-YPAGCMZgrJynp9s1FL8r28ALI7xQ2SXZ8WjOgHgVvYz9NQhuxjH3SGx9K-_JHEjwYekP2W_DFd6WPHqpR4di3OT1lJS8qCBenoRP1Fo4LgXHflr9cI_tMly5j7gLMpMq7xvP3fswMbYWOiqCeCaAooUjwAC6qiiNQ7j2jht7Lt0xfHuCRJgKEb27I9RdQnOvq8fMA9AUhxSogmto0mTRJKdedYfIuCICxe-PrNNNTF6cPG8YDZDEOM-uDwI_mqqncZwjcRSDl4fX4pGcrBS7jN2-qTIlo0rFiRp0lH5BzjvtXYy8Zo185wuYkVcx0JGZ22v8_3FiHDQ0TklVkfDyZb8T1Q09xqblqdFTyDU267rVGudgouZYJ_TZy8LabKmZ7lYCZXFUv1rzqCvcDl4boPc1xSJdHgLgMAKYbWJI29w94fYZ7Dv5KxriyNqwMlA3SLRtQg1xd6cFzy9vq69y9jnNIIAJdJf0k513nln2NNLRyZZBCoz8cWATROOKIV3nliQVNGft3aloQf8w0mx2xm3FAjTUjPSzdK_HU9-X5qLIFQHk67C5yP6kyXY0h7vrwT7v9fMLm18dgQifJ0_kQPISeF4LlTy_G5gtRCq3tHZmtB80Ph7aaGmUdHMrtutSX4amFO24jryBfKyIRrb4uCE-YPPhYsZ2yaIBpk6JlLydRpgcSHgX2XNQhtydN7IaY-L3lRBVsBs0SxfmhW5LSlD5aCsykImVnaK1U2n1rLKZtquBZGDmQvCHfnC1YSlBTG46rFhE1jRBMl603uSoA7nUswStKLDNk3rHFiBWR29DGyV6GkqUtsZaO3XXKOPIiQ9yM0rlpKoamN4j-YRS5GxQh_g-BzkqjPK-dA5XSrRvhJxWt7g56ooln6w2IPlgwLsUY0uawEXETiXrOGHfwP6xK5DqsSZdSJyar-LEGfNzHRXW4Xi38c_-JplrMahPcJEC02Zis9khCTUi6q3qHrvvz0Pf1pyAQPOd6KdE51Xthc0Sx4IqHPmg2IndTfFgigD7QWwbjnWQ7uLXNMxkOCfDYlSiE_Rdr_8SpbwPkO_TWV0iPlslQH57Agnz5XCYyBKrXQ2z7w4TOOwZ8_yGSQXy2NQKJKp0LSQ1S0gOYvr0-VGbpKQQiA0YH9vUb3DV8zws89QeUJh_cwb7bhQ5Da_8wegtbASPTcX7l6rIhggNpc0O7uY3ffpPPbaS-VIKGRmMJyTcZ3b2C1J-_uEMgpz-JRr0GEuvbiVIGLRFJQBXy6EUDd1mxHL4MAm_ut-MkcKINbdnvksi18vZvQ-jE39si3RqDKsf6x38z2hFoAAfScL6DofwP_rJoEOkThkeTFWaC_jJ-nWByeKldRGwjUGdKUO0QhGSkAmBKQZxgC1N7EAKEuKMZ1kL","summary":[{"type":"summary_text","text":"**Testing manual serve attach and stream draining**"},{"type":"summary_text","text":"**Planning real daemon regression setup**"},{"type":"summary_text","text":"**Designing session spawning via raw broker controller**"}]},{"id":"rs_07547ad47f060e5d016a58abe92560819ab2b9de2c0917249e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvzAfWXOAs0gYHSckXqYiKoWb91ryC_m6O9dh1l03x4O3XjqUSl3Oml8E7pSn0EA00Mg-T27yITDU-RzL6dDbPbl6OUHf39tDhGbc4wJ-KrkD9RnccKU3t7vroTFq1VyLt8enzldgujDl-EV-LnXfZhgdb7qewTlZ90H_Jr_Z_GoUZTrA1zsBDeFZdulPBxU3diil9D2R0WSexIdOO52XW3ciDT5nLz8lQVZz2g8x0GBBvjsBe4YvDvZ9QRMGfnuwJjerfssRSh8DbsUPnhbCrE1qkBYO9rTI7afFUA-RTYzCqhnHY9n5KEakckitRdH87ViZO5d1koMwue82oOQ8_a7-qV9oxJ91HP2AbXxV70Os3xMMRFTGEfKrFJJxf0KdQGdZnE-ThyuybdhUzkcT1E99zuL98DzUa-gK75D26N5rf3X_j2Mm6jxv-bp95CKj3NJHNd7SAqDVR2-lVKrc7W4GNyH1mIGKdTGHrSA5hD_8ffxM-_NlukYzO7k5Agz1l9EhWmhibsne2s81Sgz_hRxrgdjEv3PCM_Fw1sAP8BZ82cWpUdjTO1jm54JjeaIFTx1TxubS40nz_dLhRI8MopRcUjiIiwuMq8P-d8SDOts1vIBrQxWbti94p7JAEtQFup0T2D_mN1uOD6O0Ak9RA9-5IZoRYF_0PwtePFs2pNqaMQD31AI9lwetgDt-uVyrCxKudbV5siAPfOWSe8gumXePKcIQav5iA5k2VY-WXes8fuS9zGyJXy33uuOlwdq7ZsfHA-J92OlKxkcq0U91pS1KKnQWOYKBq7YGNgTkbXVkTWM-WG61kwZZ2yDHTIx8ybtdl1F2KhH0bYiT3zoTG3MilDvbfneJl-Qs3kUSecELtn6yP99Yt-v_ZrdSNBQPSY-V5srHMniX1ouEYD9uHHokv_mNdxfRbpzVKaZAkxwtYYirctXCJrHjMAkB3CLZU-SnkkJlT2ybazbUhOsm3JLo7hzAJpXBegLlCsQaeDHa3T4v8Lpue8AlDTHtwc9HSstimZ-lNeDAum8z4g24HdtT9lOLcGZ1IuLsO0YoMRvPZ491PDSxOYZM1Zo-kNQ4pSONFJajx2QA-PRdzAkuwFjypcYTSLo6EcjU8HogehJ2wT1zooCLsT-OAM2b35AGjZ60uveFUcUkmvR0U7b0-A2dm1t7HExVlmv2fAC7d28enPkPv5-hHtdvDKarsr58Mb6_Rmh0FYkViEMnlNhm2P6R681pmGIarsb_gbivC0wzakwMDhmNH-0QeanumcjHcCa4FDg9semTz0FHz8d3uhGuqcOTQ04784XbtV14WBmL2APHSWuI4NAtxb0CxJAIUWHMG2DaCq6zAhclhE4HA1eTjfu28tzwsjQ5gSTR4AimEBpRLwB6ny34LfK6X9cFM4gfe7bl-zU_grxBjxvx8uPyP70OGC9mz42fmezsHrrO48jQ3LI-mBawb_JtPQBcRYlIFGJ2V27PzzwGhc5GQF3MsUS-mfijwqiIg9cNV04bND1MhQSRQlhpZfreI4EzxSt-nO97soHzKyQH533UQYIEt4wtgUTN5tLRoWQ68Zf1sbpGA-9PAJclN_5K9m-uQ2FvkNH2zfjx9W6CZ6yecM6nBStEbSZb_SQUnIBI2rp2BGOG8RfHCN5XFj9o1-ZX9pC2sLttUXAl3aNUnFYp-EhWXSlIor39kj23T95Z1fliN6GMcimYSO_bS8lbywaR613_tfTPuCMY_G8DW4lpG0RTzcqQ9VeSf6iQ7HZNeFp1pVsTUSf3iaY55suW-j2_U0prZbcKUVFu02vCzziKG4bj3QXvBCQuU8dZQOhGyjZIKj8QHYOk4c_iwFN64sg-GuMQwc-HqkwygvDPgeExryh95gSPlIq4krZvMXnHR7tquOLHgktPmK_aeaVbS3nb8Htcv7l6N760IX9GGbpJ3n5iON3ELRBC2k-1HdU2jEgaaJlYZxAdXEkMRJ-lD6gcjKIqKpElCq428Q444uDXagpDLfaRiOsTcdARVImDPKoRm6ZjYdY2Caqu2rlaJ4E3DbrjRbZLmYjEmFPAsh7uWlYviaWWat4Xs_G0Vg8L1xWLxUEH7tnrnUJX0M5zoISgelF7F8hmMZBFvkNfqq5AWGYLckZwGKwuXJ62w-StpswIYHvQ98Mo5iANi_NPTocKJQ218CE0EnbJH9NbwJdqoipSn2sYAhuFVxsW0o7RtQXHT6ucfm-cVmfMUEET4vTlw1AgJ1EYHSGExCG_uizPQSCqGyp11-dIBewHsTjAHlsAKdA6VYpNgpTXxycc8Ora2DwcBkocrE8XC7s43sv_7Z_HsPKNw7m-piOHEdpX754mdySty8sIf36DRVnP3ejA9O_VkK_NgrjmNzP_CAHHEVxH6Kj-ZFOF31zzw24UHEtZLx6nrnFTMEy1QGkGsO_2cI5eProE52D3dIVvQLyyqWSPUH51X8QUiDaayyBb9uh7ofd8VxbpB4_GlHObjtLJLT81BgN4tjdwpex8glpPegg16r57pK2Zx3i1l53ok6xy9EgKKE58AYtKzITcrMWd-CSrA5TPjYOiDWGREOnNbFjHFqP9oxeNzAIYJ7jY2ZBBrjly9kDyowsizfdtAjG85WhPcqFGDKgIhQV2tI5wEbWKq9VlTo1MbAJ7qXJmZM5-FkOXO0ECvulTzIbc8XUwn9TmPjvcrdHzReK6soUvmN59fDhnJx9Jh6go906KnqY9pu2-T3qPneKas425fOkrRgXa43EpaDWeEOWwo_RkD_6dqbUpEjS-Q8vkaS8LXiMthD1L_flbKF8u-10lLD7mk8oXbitpIUKanAdwAffXKNeH1DSAV44L0H8vgq2WBGJzpyDLtLLzIbV4-59UMg5AoyFVk_s7i2Y-7tixQeiPJZ3nOWOU5btUZmzv8TwehScRsW-IQXYXHDTvEtA9zOspcfrGIp4rD0l4max9gdqfmsXVvecNIr4tFpo5MNTNKoXNemivlr4zAe6ZYF1bhXtUoLcZJvjVY36d1UB-1p7HG0O1EDPyhXfKU6XO1oD_jTjQamdNrex2PCCW9keMzD2nCsnEyiXZ4C2bHs1Q0Lk-z9xx9VAk0-IuioVQR7FEE4Gc89BUewFtWusrEd_X2oGRJI8NoEkgQ15bXUKsbTclR_OEjDHJH7pm_UjR5hNL6YGpCBFThxG17AGUKpVWpINO2BSu3cfrVG7o00_CYmS6rZLDmqaObk9WeSnEAxuQc1cSoG8nrBi4tsWI94P4RBLkHq4ofElMGsYIdtWj7rC1hcTR0OSwHW_oB5jaLFW1B8NRX3JvyC0WCQF05ebI7KYL3mOEpaFCQN7Ql9CUCKTX1fIuExivUlY-xMzUOO9HNeczz_8YbfLy5HZH7N_DNnIyhN39ZgJIuRqAuxwVXvimQVZIdbToG3oZahybJXzIAnlyEu42l-jOkIAkDK7Zfod7wOOcVujl-bb1GGUYb6L8aHPhljsk7xw9uLj2xAyhKDvUd_LdAjj9fMzA8HHOVWb65NGLUh7QRzEgI2UwYIl4t2NwNtWtDNKbx5Q-L0jRZFk4oussRl8wzw0aMAfQ7_cspgKYN5_R-D53BbFtTX9Wa8sDeMmbsOVNX-7HkoB9olmxuDdeoqgLb4eWuC_hHkvvc76zAgL3GKPqSFzP3z-0fJkowgz6gekoeMDl9oreNnh77v6LMx2DPlpQE_iXnouBmnj5K4WqsuOxuRGj23Q5nn4uqPTC9VPft-EuFLkAaEQ6GDfWVN4PxT9iVWsCvbtD0eOCFZvbQuCncp_P2wmhHPwvqO1T6RShTiUlIrZXVbg9Sum5FyiFWykqZq4GZXXIkacYPcuAzwWxzD6AF4XyihaTfOYi1Z3Up5y46D72EnzEURZlG7XqvUZN5TNf6eLNmoFoW-3azvI7OTD5drVquNzINPU7co0Gpz0PXg8kLmHuHTvwRHIsw14zKK92rUs1u3kQHSIlPIh1vgYQaguqhl72WZteCnJYo_XECE8cv7XVU7htwqrKFl7XyxzLNE5cxXUD-iVr1lBHNxV8daX0ZktGkqkoGVzAGtwTPbzAyCj-auS4g0qjS0XNm5lBWqsGXjRz4Beb1SUcmGD5ZbUbFQ2KLYJeWmMl-xj4kPbJvbRUp7aOH70skIyS-GsrTT9eLhhunw5vR3iovSJ-EP7eAj88T6SkWNcLjPzixhGLUNPMB_rND_cs6KTeqZa44LemZz2cdeoLmRE0HZeYjVCfNwXotE4K5SWhzpeLR8S9c6_Js98bkOQDPH8PzMkFhmo2EyJ-50k9tRTgallT_N6Mf83srcP-_Oy8oZGwNruiTCfiqHiK-TW53UNBy9wsIocENyH7vvNFGFdhIuGhVe2uG1gofcluW0iwZuZy7NuZP0UsC1PgvrZZtEJJDFFHDYAKEoO7DwboeVLU2q_4kvOtvO7w==","summary":[{"type":"summary_text","text":"**Designing fast ring roll with loopback streams**"},{"type":"summary_text","text":"**Planning deterministic and smoke tests for stream rollover**"},{"type":"summary_text","text":"**Noting dispatcher restart impact on stream draining**"}]},{"id":"rs_07547ad47f060e5d016a58abf3fe88819a89f0f2a6ff042944","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKv6bNuIbj7XGbwu2omLJK0jJ-1AQKfPxMlVN2dej_mE9eYawYYIBPOK-JUqN3_xofJxupjgDN73JoFu_U9RZ5NEIAtBQDC64eg_dxg9sQ9b7P1AKNAF5nnrl2J9tB2zxN9PQoA4GFWHMNheQPwibVCJiuFa8Ubwmail9VLiGDfVkNtG5plwcwH3BThX_0RGYu3Q_7OYZAsbyjDMAd9hnufQLJum5Fy8TgNcusIiXkHaErJ3xaTyOnlaSiLVKe8VOEWvtZsISSUj3X0Ou13O8WLmaWfkdzTOwo6R1EgQn5SrrSHh0SL6YnbTW0z2hY1GNv4c-YcoEtPlmlxqgXErIYI-4TaW6hnQq-MkuPPuVRDp27s48ZrxtemDUf1C_8YNq7kuGRyIS8Dzh546aeP10OESvKEQ7fnj8UeR6rta3hKH5RAdphwvbZYRkbY5R7DfACSN9bEpOei6L77JjShcbLrBZ13KEq84DWACFA5t6oMx19yj86xKiExReeDaO6hodsorL0-qVrlgoWkXldBpPUnePl0rcg2Ce4-aJiCfgdJiVaXhVeDcijc83NTOIfgY1fqSD1ZC7uJXmBiuvXfj51Wta1qMy3VgVaE5qcJQs9MuWUI_v5h-fg1-Qs8TVj16LmubYCuMUohE_zvyOI-HqPfjWIAXbDwkS6eZNmaFdgM_h3XKlrLtvEucBNKjpoPeX37aoT8zdSPjJJQXIu9xXQ_Q8gX_07I8lZwDXT85mNEZm7levCo_asnUOuU4sVUO5Xzw7puJAHotAR9yFsFlDOJMF97N55p4-SZ-YDWwdV1zrFg2EbQnV_KZdJzB2UeAkypGnxtXnAh5jet-FSWhzNLcp9zMN8kwtqv0U1bveuj0OvZOVJQqzmvf8_R7sN_tI7isl0UYMKUjh0hTevaE1hALEQrmfn80aAWfgzmVmEcV4RXFoUeSHVghmosgjhL-IjFPqhTvYBFSjQ_xHNOunAMPU6UIrgG9P3EJ9jrkx6fEMfmk4pmsBloCdu1G72ecMn03rbFQYDNjRDV_kPGAEhpkp8Ni6sVOigLwUNmsSJch_30XbjWAUlJAc6BFXv5IvY5Yhjy1oKUUQ9MCdC3cuxmrbsG90WBp2u1SZx6pVhJ7EF9m774lhJCvIdEQxs-IXbUjtGbuYD3pJEmVubiLscWl5PcCQGOQCPrH0KA42PUzvwK48_UAEhGnabPqGx8IFr52kVP68Gzyq2-6jQfWcGSlMuwadeyQl9C7GxzokdNg3JYpfo2xHQRdr1xMUkUeKjNlTbLo22ok--Cx1nHw78X0aFo0U-uOrhuO29j2NVm4uMZxMT1aowLXVSeiFxj2G-plj2XWkH_YnGjEmNWvi_pycfSwlmqyE6tstwMWbsQnraVr-5F_AMLt_EgAa15q553DWADCv4WlGNrOHy3CTOxAmvZ954RL4oG15NhlcqnfL7YWPJK0LyQOk7cW0Q-2BOLKvurzULC25Tsgr3rs3zsou6d7c9kHWleXaLpwPGPL18mcRaGjc8XOQkV0ctaD64vnTMRf_YtFuxae1_OEocODeMUlw2ZYp8MMQaM4ftf_y-9bkz6UntiuWXVh9GUgUU7n8CC2aEho8FTrM6rXjqS79jRlb_AJ0RxXrjeV7NXFvqJBYbwCwFU7bcPIUYmmWIfzoQoF_ttfob-9oPNWn1UimRrIrvcoIdnsPf18D7PmA7xJ8PLgOsCqs5xIiaQQhh-ag59b_Tuj_Nt6JLo8laPpoWiD0Fi0G4lrhruXj0LepRgkNXf6D-H7U4psUM25_xkG8aTX-immf400cB_H6QCCvI8Ci1v4YBvwQPWInH3nKqmMIlsWXGuSB8oW6xVBRZ9Ey7PtT7WXtdCJFhjr4WpdH_SE_LJq9UtndyJjheEAydZiJl_M-vUlW-iyQZ2P4kfuMxxyhBUYhe1-DwdElNWvAhvlNJ-rVia9oq-1Rw4iSLDqAtEiSlE49DpqLmVIlISZ_P4jXJAk2ddD6XVXj3GoBMG_oL9NMsk4O1plGimwGbzdy_VuXZCP8tVnmDdWgXN1W0MEW_mKdWMAdG0IawXY0e9z6MqIfUa4t4aHLokE7iHU7oAO4G4zHrHoYmtVTM-BuQS-pElkKM23NjzYeiIYRVAx3nUMCxQX1pxn54lF6F-RsdS9KV90AmaN4VjwsbSCsUab-fgqbFtS9iLP7uiS6w9mLFPHZdH5ClX14M68LDKK2usCupo8ObAJtL0z7xzdcFWvgCd4eegbf6f9ADbshfOKXDdt89pz5jr6aN-eA_eJxfkFIU2fA11Ivp__pPtK0fugppqQ5cHm1PaKjKjXa6-gP5VqeKiJbgfjffpiRBRxCKt5z_s6K8Vf7-PY4hurFZDn8-ztAeYn0MQAfHALa7k3LR2YEhNXM8rNSV-qDLmjS6elfzjeh80xz_-Gc9TbfT9hbYclnbD3BSPpNkSwVOfsa_mk1GlD7Xw8ssNPEPo6iWI15WwG_q9_Gmy2xf_eCfRzSJdtiMrPDs2wH42k-4VdigJ1fpHY1Cba1NoWx9QZWIuTCeWGRVTtiQbXXWaWaX2nahC5Xot3KR8mQwRYNh1SWUDK9f19xNNGec4WTvs=","summary":[{"type":"summary_text","text":"**Analyzing brain restart stream handling**"},{"type":"summary_text","text":"**Planning deterministic stream stall test**"}]},{"id":"fc_07547ad47f060e5d016a58abfa229c819abf25e52c20ffc304","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking broker net accessor\",\"pattern\":\"pub fn net\\\\(|fn net\\\\(\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_hb6if8NbGxTBZoUDyuP7M4cv","name":"grep"}]},"duration":37684.465600000694,"ttft":4168.167300000787,"contextSnapshot":{"promptTokens":114788,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hb6if8NbGxTBZoUDyuP7M4cv|fc_07547ad47f060e5d016a58abfa229c819abf25e52c20ffc304","toolName":"grep","startedAt":"2026-07-16T10:01:31.272Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Checking broker net accessor"},"id":"6c076eb4","parentId":"e2bbf395","timestamp":"2026-07-16T10:01:31.272Z"}
{"type":"message","id":"5f3b9696","parentId":"6c076eb4","timestamp":"2026-07-16T10:01:31.278Z","message":{"role":"toolResult","toolCallId":"call_hb6if8NbGxTBZoUDyuP7M4cv|fc_07547ad47f060e5d016a58abfa229c819abf25e52c20ffc304","toolName":"grep","content":[{"type":"text","text":" 2730|    /// REQ-DAEMON-9 self-heal `attach_net`). Lock-free.\n*2731|    pub fn net(&self) -> Option<&NetHost> {\n 2732|        self.net.get()\n 2733|    }\n 2734|"}],"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":1,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":1}],"truncated":false,"displayContent":" 2730│    /// REQ-DAEMON-9 self-heal `attach_net`). Lock-free.\n*2731│    pub fn net(&self) -> Option<&NetHost> {\n 2732│        self.net.get()\n 2733│    }\n 2734│"},"isError":false,"timestamp":1784196091277}}
{"type":"message","id":"624e04e1","parentId":"5f3b9696","timestamp":"2026-07-16T10:01:45.062Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting BrainRestart request and supervisor helpers**\n\n**Reviewing test commands and known hazard sections**"},{"type":"toolCall","id":"call_Os0LNYowGCBYnuogO1xaU0Tb|fc_07547ad47f060e5d016a58ac083134819a9923de64f9544e0c","name":"read","arguments":{"i":"Reading refresh related hazards","path":"docs/KNOWN-HAZARDS.md","selector":"623-647"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5132,"output":374,"cacheRead":111104,"cacheWrite":0,"totalTokens":116610,"reasoningTokens":335,"cost":{"input":0.025660000000000002,"output":0.01122,"cacheRead":0.055552,"cacheWrite":0,"total":0.092432}},"stopReason":"toolUse","timestamp":1784196091294,"responseId":"resp_07547ad47f060e5d016a58abfbfa00819a8353b8c1b61f17f1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ac01affc819a8b5dd2c5f201c5e5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwIx2eSXDrgNl_QdZqRDNpm9r2wCS-zI3Y6iovr2dV82T-JLrRpu3FOK6xlM5K7s7kw9OZnB3-CE9-3q7rwXDUjCfHlGnwMqmhCHzGlz7_TJSnfLFiFiAj4GdO8JyzRSoaZ7uxkq8u1Svj1-zpqDD7XVQ0paJkjzNfpvfwxvqSx44ajEZYueAVe3Tn1zBMdTh16X-EESs5krQ-4C_nLGOlLy8JhD5Rh6S0SyxHtc8FKR9TkvZa0vPUPTR-sE216C_g46ujZBYP_pxPBDMduRT71cW6Oo1dHtONWxC-PYw-vdguWp6lUmpMQILWWIrGOorD2G9yyLLhveXuR0Sz-TTRQLWih7T6S0TobK5fQrB95WyDoT4BGoBa1KHSQt1JGudHGcb_3fWN5okm9qubH19QeMXNhy3wcoSZdLyFpuYezliaSgnO_9nHMmB4dCz-XQDEiaoahZFZTrBpCv_l1dRRUaICUdLypXzCvO2c0e52rXQUVRQjZg0Qr9QCNERHbkwWa-o4y5S6-o6paMu6sBS70xKUUAKh_FNYm-KQEIXKr63BdN5_Ku4-7evPO3xsZpdCwfs1CDWEr3zsK-GSqznrDZEaiz-qwxP9eBu4N8-jg2lRklnWQ1Tl1xk64CsKFDmwJlF6yNYxPz0FIzQIzjqYWcmFCwzCSU0UOptbkrBht4L91OyGrBLi5IKH0M0dgaW8FYU7dvFsF86bfUONIifXYa9eTQeUv6TP5xW9nLaov6FK1MoMls6cP4ljzcjVUFinXIF4vT0EXUvMm1l-Pxfgt7WUpEor8OjpBioxMQ74KGbxCUXzCUAkUvPVOUk03nKmnmclODuj4C6XRGMvgI4iJKVmkIqjGnw20yjfH_uNwJBNj7xgGEpKYZW_GroN35e4F4ro0aChDIRVINJ_Jj_LyQHIN0mHmeQ3Q_xLIW3OqChFjXKHKg_jEi8XhcOk4lZ84DbmWQSUQfgRPlYCHOFC_Gijb8JPJXMpNzKsIFKSd1SSpBSiHSi_R3bIfsTLJsPuYjNpSSnO8qv-iIZgFt_1AV3dfw1mz7xhJ6_DwRti9ur7Tfkk53a9cd1fYtJ4sgKREhZEVQlw-aX7SheQMcrKYlSqLwqUC5tCqE6QCpXC104-7WpGitpVdmfS41LtyrpQSSRjLF0XLxwsWFAyLbpiTDiUmOyB-RFWPzpwqhbjHBDIQtbonkNsA4YaG4X-neTMjbPsTzvrT5hhxYgfXTm3TwKM--ZjI_xWi-DFhVpK51u3LDQGd8EcH0HD0gRUw0LA_w-_ZrX0izJfAmXdwcSmhbeqinQ4nxFuCQDF5r_ZUIST4d-H2KE_HFiozyU3tYwNsEsVpMedNw1k_TzQykRZBYRbz-Zg-GLliJ2ZxdAMGX7ornotZnymyiLve8cQ4emDsW8wfO5IUEF1IoRjlUrrAad8cfoC29zsu0_pqr61j76d76Z4Sl6G_4KYyR0QvunVyrXPOOzsJFcRLFO8ji0yuaaQymIH-ZnsvzQgi1__1SmiwGd-4Qq9q1WstnxmVnI5i2BILBfubIRVUJ6S3W691_IOkmM2Ryj2WHr1zHmIJBLgW1e3RypZ9_8OcuFx1BYWwaODwU09PtIS6nYZ2G_MF4DuswyNapUYg3UIC9zVxWqPsb4J-Z7u18YCm08yqJWKDtxybXQFievyU91YrqDaC8lK_WAHtEqzt5LqnOv8BNT96tPFihqKIUmlUp4Yg_E7X6lDVGxXGUCOxxpDpT29T14Qb6JwoDDP9MDW0eHz1XriXo8M9jq4Ovgt3u1k-CTkw2rvpBsmQ7-o2oJ_dtACzxmT7rPp7nNe0NRygbGX7d-tVlkQxRYVW8g3b4uF578OZ9i-C7Cv-vqEorQFhiQjd8sB3GCcA0xsF88nFU0cKSXt7Qv6w00ySUrmR7xX2VzLxusfgtrKq3lkyq-AuJTComhovgEOlp2J-Ls7CYeHOww0nAazYwGHExcmNamqfrEn1Lm60W5FIq7-92z1EAYGDbJmC0nadkOiIZTZ1hXk9xUhekm_FNylHNLhUtmDQpEh1ilUb8XOeF4tK_-TfjTUE2z9_PJdukVFcTzdplVf38rCFBlzLUL7sfGvBNInww4qDVat9dz_SucmybMpah7V_kcK87FTr0Al-ZKqbCCi1lwGo48Dkg8O7sKNFDCjKpa34NcKU6WR_CMjXZw7Uoqy91r6EOLeWr-RVNHV-DXau_vvGmx5GGIMgxsH_XqnMyu25NtSMj-E52XwH0elfVhQ_8EZ30wG8jVLcw7TQSHtOw1Fpst5PF1xWZQ7lR9inraQ6cJkDaqpbAKwe5KWaM9sM6axRaPH3-hj27-_qt-cZByzSN8pxBez8VUwBOagIOafknXh0qlxx4JHf7_M2hbLvA1GtAaV4sKM8wn14FNbFpLAfUOybh5OrvbWcz5W281xgcYOweIiyMIPB1bhdfEqFuaj2k59dvGnVk72nU_Xhs5fR7mhZAeGBvfy5iNFyXpvaDp0WjRmUzGesvaUzGDybe_s3vs5TyPM_Nq09a-HA5yEy_MlWzykXFgMLczMaE7f0SY7nrgRBkFLzmxh_11ikoKGq4YE4JJ5Ras1R5D1z3d0MuBz4eotfcdAwIeHpQRWSfDJMlDCaYLj4enjZKX0FpZqfYd1JlLPtnKLAYrkoAGENxtv5mMchskoiPD0Gqr_K","summary":[{"type":"summary_text","text":"**Inspecting BrainRestart request and supervisor helpers**"},{"type":"summary_text","text":"**Reviewing test commands and known hazard sections**"}]},{"id":"fc_07547ad47f060e5d016a58ac083134819a9923de64f9544e0c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh related hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"623-647\"}","call_id":"call_Os0LNYowGCBYnuogO1xaU0Tb","name":"read"}]},"duration":13765.3423000006,"ttft":6369.995200000703,"contextSnapshot":{"promptTokens":116236,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Os0LNYowGCBYnuogO1xaU0Tb|fc_07547ad47f060e5d016a58ac083134819a9923de64f9544e0c","toolName":"read","startedAt":"2026-07-16T10:01:45.063Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading refresh related hazards"},"id":"f4c16dcf","parentId":"624e04e1","timestamp":"2026-07-16T10:01:45.063Z"}
{"type":"message","id":"86e35f3b","parentId":"f4c16dcf","timestamp":"2026-07-16T10:01:45.110Z","message":{"role":"toolResult","toolCallId":"call_Os0LNYowGCBYnuogO1xaU0Tb|fc_07547ad47f060e5d016a58ac083134819a9923de64f9544e0c","toolName":"read","content":[{"type":"text","text":"### 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…\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…\n\n…\n\n\n[Showing lines 622-650 of 736. Use :651 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"- **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).\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\r\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.\r\n\r\n---\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\r\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…","truncated":true,"truncatedBy":"lines","totalLines":115,"totalBytes":17521,"outputLines":29,"outputBytes":17521,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"### 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…\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).\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\r\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.\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\r\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.\r\n\r\n---\r\n\r\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]`\r\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\r\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…\n\n…\n","startLine":618,"lineNumbers":[618,619,null,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,653,null,655]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":736,"totalBytes":17521,"outputLines":29,"outputBytes":17521,"shownRange":{"start":622,"end":650},"nextOffset":651},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784196105109}}
{"type":"message","id":"0fe00546","parentId":"86e35f3b","timestamp":"2026-07-16T10:01:58.750Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning ADR 0018 for net consumers**"},{"type":"toolCall","id":"call_4VgHXuy2p1jGnSZI1hNKmtUz|fc_07547ad47f060e5d016a58ac141628819aa9d5a684d8070684","name":"read","arguments":{"i":"Reading production dispatcher attach tests","path":"crates/spt-daemon/tests/dispatch.rs","selector":"410-620"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6549,"output":85,"cacheRead":113152,"cacheWrite":0,"totalTokens":119786,"reasoningTokens":43,"cost":{"input":0.032745,"output":0.00255,"cacheRead":0.056575999999999994,"cacheWrite":0,"total":0.09187100000000001}},"stopReason":"toolUse","timestamp":1784196105136,"responseId":"resp_07547ad47f060e5d016a58ac099e74819ab4fbbbff001a3f44","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ac0d5b00819ab1b13b01aaf9fc97","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwUlhi2Nao-KNjSCEmDu9NC09EZb474QNm9X4alggVYifu8nbbpXCOI5EjEN4wJFLucobrUZKl3oKvd3z-4dS8L-EJx16me8-K5-e98QAVL8-HwZCNqU7kTSw9KKXXx0-1bFyqbMRiqRvAfeh8wymc02LpbQ6xSloyfCYsCp0GTVDZvpurGzTMDizEesX7BhKtc80YQecsxU-SrUyXEbf76vxHlDfBJqniWj9a8XNpCLVkYzZ5TA82rQdrgEGVkniZ5VpcSePBsWfXLmcKAwm7YOGaofx0K7T7txtjQGeegDEwSQ9afNoSkpcXavKA7XCoS5kGQbOf3f1-EE6kzr7Z_ATADmuYIFhVdbDR3X-qpYLkx_PunjpCIUdHvLzs55zABhG7HDNQlBvgEfruWTrDl_AigjkNwNHnkTcRjH0Varp-Q1qqchYY-ZZwU70NIGlaHsx2o0OWpYoKRJzM4h--gT9T0UDlfB0GVm0xFS_RNBK9Wk_upDHSpTmy4Fsmm_SX0QnvRiwAMEtsnH74Ho5r77uyn5pIosf6okL8hadfIEWxiL1ebzLYoxgXOQJ8SCQTLmX973rY3LXTf92J0CJIbiB8Rhf6EElq505zOsK4Re_QXInDsrqcYkjF0CfNRdcPcguPTcrHSVG5x1Eyrc3a596BGnD9ki3CPzZnWnyS17eVA8j9j_5dXr169CxFz33eyzdSgQgNjcy7eSU4nd_-Tyey3eUp00P84XHefdHK-8v9t51I2ggcCt9fCDSMEHzNQUSnHK3cftPQ6HKs_lVouyzSkySiesKDG2_vVASiWB669bUnGBvKBRaFG9s3ckfQuOhhRYVfG9OoQGvnymoroNn_pooun5OYaPWtnXh_XDpsJ5YLneeO_upB7LnjKb1SaOupTskljnKzMv2f3Rbv-I51ZRRbkijI-Vid8bflpXbUlZQIpUZuv3DJSFASnCsPb2ssE4QVol7Q2GNCvQC8CMHI55Xr4rbId_T5_2lnFqLWb95PsmXFszGkJOqxo1_ZRA5WYsDyt45rCh0KVSBAyhVf_QFhE-s6GndU6nQXs68gBxQ68CHvW6cNOYnMjUWDY5upTZjejfZSLcnW4VA4GyXDvd42z9JSLnsXrwsI4kYc=","summary":[{"type":"summary_text","text":"**Planning ADR 0018 for net consumers**"}]},{"id":"fc_07547ad47f060e5d016a58ac141628819aa9d5a684d8070684","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading production dispatcher attach tests\",\"path\":\"crates/spt-daemon/tests/dispatch.rs\",\"selector\":\"410-620\"}","call_id":"call_4VgHXuy2p1jGnSZI1hNKmtUz","name":"read"}]},"duration":13580.973999999464,"ttft":4205.894499999471,"contextSnapshot":{"promptTokens":119701,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4VgHXuy2p1jGnSZI1hNKmtUz|fc_07547ad47f060e5d016a58ac141628819aa9d5a684d8070684","toolName":"read","startedAt":"2026-07-16T10:01:58.750Z","args":{"path":"crates/spt-daemon/tests/dispatch.rs"},"intent":"Reading production dispatcher attach tests"},"id":"ef651f76","parentId":"0fe00546","timestamp":"2026-07-16T10:01:58.750Z"}
{"type":"message","id":"5696be30","parentId":"ef651f76","timestamp":"2026-07-16T10:01:58.761Z","message":{"role":"toolResult","toolCallId":"call_4VgHXuy2p1jGnSZI1hNKmtUz|fc_07547ad47f060e5d016a58ac141628819aa9d5a684d8070684","toolName":"read","content":[{"type":"text","text":"fn dispatcher_funnels_wan_messages_undriven() {\n…\n    assert!(\n        spooled,\n        \"the dispatcher funneled the WAN message into the spool\"\n    );\n    stop.store(true, Ordering::Relaxed);\n}\n\n// [int->REQ-INST-8] [int->REQ-REACH-1] remote-drive is served end to end by\n// the dispatcher: B hosts a LABELED session; A attaches by session id, types,\n// and reads the echo — the gate subject came from the broker's table (the\n// spawn label), not wire bytes. An UNLABELED session refuses, fail-closed.\n#[test]\nfn dispatcher_serves_a_remote_drive_attach_undriven() {\n    init_home();\n    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n    let dir = TempDir::new().unwrap();\n    let (a_name, b_name) = (unique_name(), unique_name());\n    let _a = net_broker(&a_name, dir.path());\n    let _b = net_broker(&b_name, dir.path());\n    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n\n    // B hosts one labeled echo session and one unlabeled one.\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    let mut b_host = connect_retry(&b_name);\n    let labeled = b_host\n        .spawn_session(spt_daemon::msg::SpawnReq {\n            program: program.clone(),\n            args: args.clone(),\n            rows: 24,\n            cols: 80,\n            endpoint: \"ling-drive\".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 labeled\");\n    let unlabeled = b_host\n        .spawn_session(spt_daemon::msg::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        .expect(\"spawn unlabeled\");\n\n    let mut a = connect_retry(&a_name);\n    let mut b_probe = connect_retry(&b_name);\n    let addr = b_probe.net_status().expect(\"status\").addr;\n    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n\n    // Unlabeled session: the dispatcher refuses (our read side finishes\n    // without a single Output record).\n    let refused = spt_daemon::attach::request_attach(&mut a, conn.conn_id, unlabeled, 0, MintedOp::new(Minter::Rc, op()), spt_net::net::attach::AttachIntent::Control)\n        .expect(\"open refused viewport\");\n    a.net_stream_subscribe(refused, 0).expect(\"subscribe\");\n    loop {\n        match a.read_event().expect(\"event\") {\n            spt_daemon::brain::BrokerEvent::NetStreamEof { stream_id, .. }\n                if stream_id == refused =>\n            {\n                break; // refused: clean finish, no output ever\n            }\n            spt_daemon::brain::BrokerEvent::NetStreamData {\n                stream_id, bytes, ..\n            } if stream_id == refused && !bytes.is_empty() => {\n                panic!(\"an unlabeled session must never serve output\");\n            }\n            _ => {}\n        }\n    }\n\n    // Labeled session: attach, type, read the echo back — all undriven.\n    let viewport = spt_daemon::attach::request_attach(&mut a, conn.conn_id, labeled, 0, MintedOp::new(Minter::Rc, op()), spt_net::net::attach::AttachIntent::Control)\n        .expect(\"open viewport\");\n    a.net_stream_subscribe(viewport, 0).expect(\"subscribe\");\n    spt_daemon::attach::send_attach_input(&mut a, viewport, b\"dispatch-driven\\r\\n\", op())\n        .expect(\"type\");\n    let mut echoed = Vec::new();\n    let deadline = std::time::Instant::now() + Duration::from_secs(10);\n    let mut decoder = spt_net::net::attach::AttachDecoder::new();\n    while std::time::Instant::now() < deadline {\n        match a.read_event().expect(\"event\") {\n            spt_daemon::brain::BrokerEvent::NetStreamData {\n                stream_id, bytes, ..\n            } if stream_id == viewport => {\n                for rec in decoder.push(&bytes) {\n                    if let spt_net::net::attach::AttachRecord::Output { data_b64, .. } = rec {\n                        echoed.extend(spt_daemon::msg::decode_bytes(&data_b64).expect(\"b64\"));\n                    }\n                }\n                if String::from_utf8_lossy(&echoed).contains(\"dispatch-driven\") {\n                    break;\n                }\n            }\n            _ => {}\n        }\n    }\n    assert!(\n        String::from_utf8_lossy(&echoed).contains(\"dispatch-driven\"),\n        \"the dispatcher served the viewport undriven; got {:?}\",\n        String::from_utf8_lossy(&echoed)\n    );\n    stop.store(true, Ordering::Relaxed);\n}\n\n// [int->REQ-RC-CROSS-NODE-ATTACH] the cross-node leg's SERVER half: an operator\n// that cannot know B's local session id attaches BY ENDPOINT (session_id = 0\n// placeholder + endpoint_id), and B resolves endpoint→its own session id\n// server-side (it is authoritative for its table) — the echo returns, proving\n// the attach landed on the right session without A ever naming a session id.\n// A named endpoint with NO live session on B (the D6 stale-registry-row case)\n// refuses cleanly: the stream finishes with no output, never a false attach.\n#[test]\nfn dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint() {\n    init_home();\n    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n    let dir = TempDir::new().unwrap();\n    let (a_name, b_name) = (unique_name(), unique_name());\n    let _a = net_broker(&a_name, dir.path());\n    let _b = net_broker(&b_name, dir.path());\n    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\n\n    // B hosts a live labeled echo session — but A is NOT told its session id.\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    let mut b_host = connect_retry(&b_name);\n    let _session = b_host\n        .spawn_session(spt_daemon::msg::SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: \"ling-remote\".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 labeled\");\n\n    let mut a = connect_retry(&a_name);\n    let mut b_probe = connect_retry(&b_name);\n    let addr = b_probe.net_status().expect(\"status\").addr;\n    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n\n    // Attach BY ENDPOINT (session_id = 0 placeholder). B resolves it server-side.\n    let viewport = spt_daemon::attach::request_attach_endpoint(\n        &mut a,\n        conn.conn_id,\n        0,\n        0,\n        MintedOp::new(Minter::Rc, op()),\n        spt_net::net::attach::AttachIntent::Control,\n        Some(\"ling-remote\"),\n    )\n    .expect(\"open endpoint viewport\");\n    a.net_stream_subscribe(viewport, 0).expect(\"subscribe\");\n    spt_daemon::attach::send_attach_input(&mut a, viewport, b\"by-endpoint\\r\\n\", op())\n        .expect(\"type\");\n    let mut echoed = Vec::new();\n    let deadline = std::time::Instant::now() + Duration::from_secs(20);\n    let mut decoder = spt_net::net::attach::AttachDecoder::new();\n    while std::time::Instant::now() < deadline\n        && !String::from_utf8_lossy(&echoed).contains(\"by-endpoint\")\n    {\n        // Bounded read so a refuse (EOF, no output) can never wedge the test.\n        if let Ok(ev) =\n            a.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))\n        {\n            match ev {\n                spt_daemon::brain::BrokerEvent::NetStreamData {\n                    stream_id, bytes, ..\n                } if stream_id == viewport => {\n                    for rec in decoder.push(&bytes) {\n                        if let spt_net::net::attach::AttachRecord::Output { data_b64, .. } = rec {\n                            echoed.extend(spt_daemon::msg::decode_bytes(&data_b64).expect(\"b64\"));\n                        }\n                    }\n                }\n                spt_daemon::brain::BrokerEvent::NetStreamEof { stream_id, .. }\n                    if stream_id == viewport =>\n                {\n                    break; // serve closed before echoing — the assert below fails loud\n                }\n                _ => {}\n            }\n        }\n    }\n    assert!(\n        String::from_utf8_lossy(&echoed).contains(\"by-endpoint\"),\n        \"B resolved endpoint→session server-side and served the viewport; got {:?}\",\n        String::from_utf8_lossy(&echoed)\n    );\n    // Detach the viewport cleanly before the refuse probe.\n    let _ = a.net_stream_send(viewport, &[], None, true);\n\n    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\n    let ghost = spt_daemon::attach::request_attach_endpoint(\n        &mut a,\n        conn.conn_id,\n        0,\n…\n    )\n    .expect(\"open ghost viewport\");\n…\n}\n\n[Showing lines 409-623 of 1157. Use :624 to continue]"}],"details":{"truncation":{"content":"    assert!(\r\n        spooled,\r\n        \"the dispatcher funneled the WAN message into the spool\"\r\n    );\r\n    stop.store(true, Ordering::Relaxed);\r\n}\r\n\r\n// [int->REQ-INST-8] [int->REQ-REACH-1] remote-drive is served end to end by\r\n// the dispatcher: B hosts a LABELED session; A attaches by session id, types,\r\n// and reads the echo — the gate subject came from the broker's table (the\r\n// spawn label), not wire bytes. An UNLABELED session refuses, fail-closed.\r\n#[test]\r\nfn dispatcher_serves_a_remote_drive_attach_undriven() {\r\n    init_home();\r\n    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\r\n    let dir = TempDir::new().unwrap();\r\n    let (a_name, b_name) = (unique_name(), unique_name());\r\n    let _a = net_broker(&a_name, dir.path());\r\n    let _b = net_broker(&b_name, dir.path());\r\n    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\r\n\r\n    // B hosts one labeled echo session and one unlabeled one.\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    let mut b_host = connect_retry(&b_name);\r\n    let labeled = b_host\r\n        .spawn_session(spt_daemon::msg::SpawnReq {\r\n            program: program.clone(),\r\n            args: args.clone(),\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"ling-drive\".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 labeled\");\r\n    let unlabeled = b_host\r\n        .spawn_session(spt_daemon::msg::SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: String::new(),\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 unlabeled\");\r\n\r\n    let mut a = connect_retry(&a_name);\r\n    let mut b_probe = connect_retry(&b_name);\r\n    let addr = b_probe.net_status().expect(\"status\").addr;\r\n    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\r\n\r\n    // Unlabeled session: the dispatcher refuses (our read side finishes\r\n    // without a single Output record).\r\n    let refused = spt_daemon::attach::request_attach(&mut a, conn.conn_id, unlabeled, 0, MintedOp::new(Minter::Rc, op()), spt_net::net::attach::AttachIntent::Control)\r\n        .expect(\"open refused viewport\");\r\n    a.net_stream_subscribe(refused, 0).expect(\"subscribe\");\r\n    loop {\r\n        match a.read_event().expect(\"event\") {\r\n            spt_daemon::brain::BrokerEvent::NetStreamEof { stream_id, .. }\r\n                if stream_id == refused =>\r\n            {\r\n                break; // refused: clean finish, no output ever\r\n            }\r\n            spt_daemon::brain::BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == refused && !bytes.is_empty() => {\r\n                panic!(\"an unlabeled session must never serve output\");\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // Labeled session: attach, type, read the echo back — all undriven.\r\n    let viewport = spt_daemon::attach::request_attach(&mut a, conn.conn_id, labeled, 0, MintedOp::new(Minter::Rc, op()), spt_net::net::attach::AttachIntent::Control)\r\n        .expect(\"open viewport\");\r\n    a.net_stream_subscribe(viewport, 0).expect(\"subscribe\");\r\n    spt_daemon::attach::send_attach_input(&mut a, viewport, b\"dispatch-driven\\r\\n\", op())\r\n        .expect(\"type\");\r\n    let mut echoed = Vec::new();\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(10);\r\n    let mut decoder = spt_net::net::attach::AttachDecoder::new();\r\n    while std::time::Instant::now() < deadline {\r\n        match a.read_event().expect(\"event\") {\r\n            spt_daemon::brain::BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == viewport => {\r\n                for rec in decoder.push(&bytes) {\r\n                    if let spt_net::net::attach::AttachRecord::Output { data_b64, .. } = rec {\r\n                        echoed.extend(spt_daemon::msg::decode_bytes(&data_b64).expect(\"b64\"));\r\n                    }\r\n                }\r\n                if String::from_utf8_lossy(&echoed).contains(\"dispatch-driven\") {\r\n                    break;\r\n                }\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n    assert!(\r\n        String::from_utf8_lossy(&echoed).contains(\"dispatch-driven\"),\r\n        \"the dispatcher served the viewport undriven; got {:?}\",\r\n        String::from_utf8_lossy(&echoed)\r\n    );\r\n    stop.store(true, Ordering::Relaxed);\r\n}\r\n\r\n// [int->REQ-RC-CROSS-NODE-ATTACH] the cross-node leg's SERVER half: an operator\r\n// that cannot know B's local session id attaches BY ENDPOINT (session_id = 0\r\n// placeholder + endpoint_id), and B resolves endpoint→its own session id\r\n// server-side (it is authoritative for its table) — the echo returns, proving\r\n// the attach landed on the right session without A ever naming a session id.\r\n// A named endpoint with NO live session on B (the D6 stale-registry-row case)\r\n// refuses cleanly: the stream finishes with no output, never a false attach.\r\n#[test]\r\nfn dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint() {\r\n    init_home();\r\n    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\r\n    let dir = TempDir::new().unwrap();\r\n    let (a_name, b_name) = (unique_name(), unique_name());\r\n    let _a = net_broker(&a_name, dir.path());\r\n    let _b = net_broker(&b_name, dir.path());\r\n    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\r\n\r\n    // B hosts a live labeled echo session — but A is NOT told its session id.\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    let mut b_host = connect_retry(&b_name);\r\n    let _session = b_host\r\n        .spawn_session(spt_daemon::msg::SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"ling-remote\".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 labeled\");\r\n\r\n    let mut a = connect_retry(&a_name);\r\n    let mut b_probe = connect_retry(&b_name);\r\n    let addr = b_probe.net_status().expect(\"status\").addr;\r\n    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\r\n\r\n    // Attach BY ENDPOINT (session_id = 0 placeholder). B resolves it server-side.\r\n    let viewport = spt_daemon::attach::request_attach_endpoint(\r\n        &mut a,\r\n        conn.conn_id,\r\n        0,\r\n        0,\r\n        MintedOp::new(Minter::Rc, op()),\r\n        spt_net::net::attach::AttachIntent::Control,\r\n        Some(\"ling-remote\"),\r\n    )\r\n    .expect(\"open endpoint viewport\");\r\n    a.net_stream_subscribe(viewport, 0).expect(\"subscribe\");\r\n    spt_daemon::attach::send_attach_input(&mut a, viewport, b\"by-endpoint\\r\\n\", op())\r\n        .expect(\"type\");\r\n    let mut echoed = Vec::new();\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(20);\r\n    let mut decoder = spt_net::net::attach::AttachDecoder::new();\r\n    while std::time::Instant::now() < deadline\r\n        && !String::from_utf8_lossy(&echoed).contains(\"by-endpoint\")\r\n    {\r\n        // Bounded read so a refuse (EOF, no output) can never wedge the test.\r\n        if let Ok(ev) =\r\n            a.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))\r\n        {\r\n            match ev {\r\n                spt_daemon::brain::BrokerEvent::NetStreamData {\r\n                    stream_id, bytes, ..\r\n                } if stream_id == viewport => {\r\n                    for rec in decoder.push(&bytes) {\r\n                        if let spt_net::net::attach::AttachRecord::Output { data_b64, .. } = rec {\r\n                            echoed.extend(spt_daemon::msg::decode_bytes(&data_b64).expect(\"b64\"));\r\n                        }\r\n                    }\r\n                }\r\n                spt_daemon::brain::BrokerEvent::NetStreamEof { stream_id, .. }\r\n                    if stream_id == viewport =>\r\n                {\r\n                    break; // serve closed before echoing — the assert below fails loud\r\n                }\r\n                _ => {}\r\n            }\r\n        }\r\n    }\r\n    assert!(\r\n        String::from_utf8_lossy(&echoed).contains(\"by-endpoint\"),\r\n        \"B resolved endpoint→session server-side and served the viewport; got {:?}\",\r\n        String::from_utf8_lossy(&echoed)\r\n    );\r\n    // Detach the viewport cleanly before the refuse probe.\r\n    let _ = a.net_stream_send(viewport, &[], None, true);\r\n\r\n    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\r\n    let ghost = spt_daemon::attach::request_attach_endpoint(\r\n        &mut a,\r\n        conn.conn_id,\r\n        0,\r","truncated":true,"truncatedBy":"lines","totalLines":749,"totalBytes":9261,"outputLines":215,"outputBytes":9261,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn dispatcher_funnels_wan_messages_undriven() {\n…\n    assert!(\r\n        spooled,\r\n        \"the dispatcher funneled the WAN message into the spool\"\r\n    );\r\n    stop.store(true, Ordering::Relaxed);\r\n}\r\n\r\n// [int->REQ-INST-8] [int->REQ-REACH-1] remote-drive is served end to end by\r\n// the dispatcher: B hosts a LABELED session; A attaches by session id, types,\r\n// and reads the echo — the gate subject came from the broker's table (the\r\n// spawn label), not wire bytes. An UNLABELED session refuses, fail-closed.\r\n#[test]\r\nfn dispatcher_serves_a_remote_drive_attach_undriven() {\r\n    init_home();\r\n    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\r\n    let dir = TempDir::new().unwrap();\r\n    let (a_name, b_name) = (unique_name(), unique_name());\r\n    let _a = net_broker(&a_name, dir.path());\r\n    let _b = net_broker(&b_name, dir.path());\r\n    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\r\n\r\n    // B hosts one labeled echo session and one unlabeled one.\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    let mut b_host = connect_retry(&b_name);\r\n    let labeled = b_host\r\n        .spawn_session(spt_daemon::msg::SpawnReq {\r\n            program: program.clone(),\r\n            args: args.clone(),\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"ling-drive\".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 labeled\");\r\n    let unlabeled = b_host\r\n        .spawn_session(spt_daemon::msg::SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: String::new(),\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 unlabeled\");\r\n\r\n    let mut a = connect_retry(&a_name);\r\n    let mut b_probe = connect_retry(&b_name);\r\n    let addr = b_probe.net_status().expect(\"status\").addr;\r\n    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\r\n\r\n    // Unlabeled session: the dispatcher refuses (our read side finishes\r\n    // without a single Output record).\r\n    let refused = spt_daemon::attach::request_attach(&mut a, conn.conn_id, unlabeled, 0, MintedOp::new(Minter::Rc, op()), spt_net::net::attach::AttachIntent::Control)\r\n        .expect(\"open refused viewport\");\r\n    a.net_stream_subscribe(refused, 0).expect(\"subscribe\");\r\n    loop {\r\n        match a.read_event().expect(\"event\") {\r\n            spt_daemon::brain::BrokerEvent::NetStreamEof { stream_id, .. }\r\n                if stream_id == refused =>\r\n            {\r\n                break; // refused: clean finish, no output ever\r\n            }\r\n            spt_daemon::brain::BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == refused && !bytes.is_empty() => {\r\n                panic!(\"an unlabeled session must never serve output\");\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // Labeled session: attach, type, read the echo back — all undriven.\r\n    let viewport = spt_daemon::attach::request_attach(&mut a, conn.conn_id, labeled, 0, MintedOp::new(Minter::Rc, op()), spt_net::net::attach::AttachIntent::Control)\r\n        .expect(\"open viewport\");\r\n    a.net_stream_subscribe(viewport, 0).expect(\"subscribe\");\r\n    spt_daemon::attach::send_attach_input(&mut a, viewport, b\"dispatch-driven\\r\\n\", op())\r\n        .expect(\"type\");\r\n    let mut echoed = Vec::new();\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(10);\r\n    let mut decoder = spt_net::net::attach::AttachDecoder::new();\r\n    while std::time::Instant::now() < deadline {\r\n        match a.read_event().expect(\"event\") {\r\n            spt_daemon::brain::BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == viewport => {\r\n                for rec in decoder.push(&bytes) {\r\n                    if let spt_net::net::attach::AttachRecord::Output { data_b64, .. } = rec {\r\n                        echoed.extend(spt_daemon::msg::decode_bytes(&data_b64).expect(\"b64\"));\r\n                    }\r\n                }\r\n                if String::from_utf8_lossy(&echoed).contains(\"dispatch-driven\") {\r\n                    break;\r\n                }\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n    assert!(\r\n        String::from_utf8_lossy(&echoed).contains(\"dispatch-driven\"),\r\n        \"the dispatcher served the viewport undriven; got {:?}\",\r\n        String::from_utf8_lossy(&echoed)\r\n    );\r\n    stop.store(true, Ordering::Relaxed);\r\n}\r\n\r\n// [int->REQ-RC-CROSS-NODE-ATTACH] the cross-node leg's SERVER half: an operator\r\n// that cannot know B's local session id attaches BY ENDPOINT (session_id = 0\r\n// placeholder + endpoint_id), and B resolves endpoint→its own session id\r\n// server-side (it is authoritative for its table) — the echo returns, proving\r\n// the attach landed on the right session without A ever naming a session id.\r\n// A named endpoint with NO live session on B (the D6 stale-registry-row case)\r\n// refuses cleanly: the stream finishes with no output, never a false attach.\r\n#[test]\r\nfn dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint() {\r\n    init_home();\r\n    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\r\n    let dir = TempDir::new().unwrap();\r\n    let (a_name, b_name) = (unique_name(), unique_name());\r\n    let _a = net_broker(&a_name, dir.path());\r\n    let _b = net_broker(&b_name, dir.path());\r\n    let stop = spawn_dispatcher_for(&b_name, &dir.path().join(\"scratch-b\"));\r\n\r\n    // B hosts a live labeled echo session — but A is NOT told its session id.\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    let mut b_host = connect_retry(&b_name);\r\n    let _session = b_host\r\n        .spawn_session(spt_daemon::msg::SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"ling-remote\".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 labeled\");\r\n\r\n    let mut a = connect_retry(&a_name);\r\n    let mut b_probe = connect_retry(&b_name);\r\n    let addr = b_probe.net_status().expect(\"status\").addr;\r\n    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\r\n\r\n    // Attach BY ENDPOINT (session_id = 0 placeholder). B resolves it server-side.\r\n    let viewport = spt_daemon::attach::request_attach_endpoint(\r\n        &mut a,\r\n        conn.conn_id,\r\n        0,\r\n        0,\r\n        MintedOp::new(Minter::Rc, op()),\r\n        spt_net::net::attach::AttachIntent::Control,\r\n        Some(\"ling-remote\"),\r\n    )\r\n    .expect(\"open endpoint viewport\");\r\n    a.net_stream_subscribe(viewport, 0).expect(\"subscribe\");\r\n    spt_daemon::attach::send_attach_input(&mut a, viewport, b\"by-endpoint\\r\\n\", op())\r\n        .expect(\"type\");\r\n    let mut echoed = Vec::new();\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(20);\r\n    let mut decoder = spt_net::net::attach::AttachDecoder::new();\r\n    while std::time::Instant::now() < deadline\r\n        && !String::from_utf8_lossy(&echoed).contains(\"by-endpoint\")\r\n    {\r\n        // Bounded read so a refuse (EOF, no output) can never wedge the test.\r\n        if let Ok(ev) =\r\n            a.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))\r\n        {\r\n            match ev {\r\n                spt_daemon::brain::BrokerEvent::NetStreamData {\r\n                    stream_id, bytes, ..\r\n                } if stream_id == viewport => {\r\n                    for rec in decoder.push(&bytes) {\r\n                        if let spt_net::net::attach::AttachRecord::Output { data_b64, .. } = rec {\r\n                            echoed.extend(spt_daemon::msg::decode_bytes(&data_b64).expect(\"b64\"));\r\n                        }\r\n                    }\r\n                }\r\n                spt_daemon::brain::BrokerEvent::NetStreamEof { stream_id, .. }\r\n                    if stream_id == viewport =>\r\n                {\r\n                    break; // serve closed before echoing — the assert below fails loud\r\n                }\r\n                _ => {}\r\n            }\r\n        }\r\n    }\r\n    assert!(\r\n        String::from_utf8_lossy(&echoed).contains(\"by-endpoint\"),\r\n        \"B resolved endpoint→session server-side and served the viewport; got {:?}\",\r\n        String::from_utf8_lossy(&echoed)\r\n    );\r\n    // Detach the viewport cleanly before the refuse probe.\r\n    let _ = a.net_stream_send(viewport, &[], None, true);\r\n\r\n    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\r\n    let ghost = spt_daemon::attach::request_attach_endpoint(\r\n        &mut a,\r\n        conn.conn_id,\r\n        0,\r\n…\n    )\n    .expect(\"open ghost viewport\");\n…\n}","startLine":347,"lineNumbers":[347,null,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,null,628,629,null,658]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1157,"totalBytes":9261,"outputLines":215,"outputBytes":9261,"shownRange":{"start":409,"end":623},"nextOffset":624}}},"isError":false,"timestamp":1784196118760}}
{"type":"message","id":"2dadef04","parentId":"5696be30","timestamp":"2026-07-16T10:02:12.182Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting daemon_refresh_e2e setup details**"},{"type":"toolCall","id":"call_qhq0vxkTxfU8KaghB4qABMN1|fc_07547ad47f060e5d016a58ac22ad20819a8324708ac9520281","name":"read","arguments":{"i":"Reading refresh E2E assertions","path":"crates/spt/tests/daemon_refresh_e2e.rs","selector":"80-305"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5843,"output":83,"cacheRead":116224,"cacheWrite":0,"totalTokens":122150,"reasoningTokens":38,"cost":{"input":0.029215,"output":0.00249,"cacheRead":0.058112,"cacheWrite":0,"total":0.08981700000000001}},"stopReason":"toolUse","timestamp":1784196118766,"responseId":"resp_07547ad47f060e5d016a58ac1d3c14819a9e6442dc6632df33","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ac20d5b8819a9e8250dcedc77a70","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwi2DmAqpImCF_qWRW82trK4VENRKWDLoBwFOlFAvTfzJcBISwOgoQ_sqobCoCY67u0wg3-8_0-aPhRc9OFxtNakDl6Yu20Ae0dPbSc0ifltrc1iGaKbGPYtuIXLeYTw30k-atJEkjaG9nL6HdCumrPoXDhiNy9Hf3ZU9GGLQPAm7nGIrbwpfX3jBk087QVa-ZWICygjjjmFRc4amvla5DypXgzRGCNLhkUOv6PqsEua4n9P2pf1cKw_l6PxCCp1KOl9PBhujYpHxQ6k1XnIh32glBIEVmw7w6FxJzCulKiN7ZE1DoB8_O4j4oYqwTx66Pz4XtUuOH5GfG1VlpKaQlNIh24uBa4w3YcWbkp-Ylh2nLD0SqLEIHtyTzQosOCD_1-OgzSYdt1lze_M1UXS3Y_Kln8U_ilEilb7N40_xVHpj1orVVYgJ0bqiQP9FImvfkVnli47oYUOkCY5SuWrq8aO697Z0u9EsMenYopm2QKsey7LO14F6uLZYcC-32qW0QCpVFYm9UKytyC5fkv_bwKwNC1EnzXeVoUmWneyqc85cJN7kUoqESs0V8lHhgXoojeaG1VcmetQK20XgCU-BMitm46PK_29hv3IToCL2gP4o5Iplzw-U9uPt7TnHy_j52475CAe6rgob91zKskjSCiIGFrEBdvQ-m9N1Ibk3U53ZT-jI8fVVmwZWZ5cQp6kgn0myy-PXpOGYdVHfYoN-pDzgsI1npzlY2ppCp_qB9XUrCCT_CbbvXWuzj0KyYBXzd-d8hCCWUryuUxxyHdR10IehRwBZSsKwWDO3O-k6GXslftj9SBkWC0pBWRTfmyDzrnQyJ9EIVHm6bE5-6xAdWeCkFJbPrgJ1aghLgewPjClaf9XGoTdt7xLUBCFj6zUD0IhXoYF7dLvEt5ACX0nZ0dWJTSDMZmcD2zuo_Sn0OPatkkjumFTzm1g3N6tbsi8Vu0lhJb53yj_2CfcKchPggeux5babrSL3A7zzSNuOWJE-RwKOVOWWQDF6UmC4OgmcpzkBUrHE2QyS-g8B_uBciFrBJo1P2K7UIWIlVaVe2njm20bPWqp6tAINgsA0vJgqhWW_JCFaKgi_RnWVp1edME-NzTJAi_GHnHOOBYDwv8DAtxRm1mcGiclHq36l6DZuxn","summary":[{"type":"summary_text","text":"**Inspecting daemon_refresh_e2e setup details**"}]},{"id":"fc_07547ad47f060e5d016a58ac22ad20819a8324708ac9520281","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh E2E assertions\",\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"selector\":\"80-305\"}","call_id":"call_qhq0vxkTxfU8KaghB4qABMN1","name":"read"}]},"duration":13409.534799999557,"ttft":10046.134099999443,"contextSnapshot":{"promptTokens":122067,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qhq0vxkTxfU8KaghB4qABMN1|fc_07547ad47f060e5d016a58ac22ad20819a8324708ac9520281","toolName":"read","startedAt":"2026-07-16T10:02:12.182Z","args":{"path":"crates/spt/tests/daemon_refresh_e2e.rs"},"intent":"Reading refresh E2E assertions"},"id":"5116327b","parentId":"2dadef04","timestamp":"2026-07-16T10:02:12.182Z"}
{"type":"message","id":"fae59d4a","parentId":"5116327b","timestamp":"2026-07-16T10:02:12.187Z","message":{"role":"toolResult","toolCallId":"call_qhq0vxkTxfU8KaghB4qABMN1|fc_07547ad47f060e5d016a58ac22ad20819a8324708ac9520281","toolName":"read","content":[{"type":"text","text":"fn 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":"fn daemon_refresh_cycles_brain_while_hosted_endpoint_survives() {\n    let now_ms = || {\n        std::time::SystemTime::now()\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":76,"lineNumbers":[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":1784196132187}}
{"type":"message","id":"3a56899c","parentId":"fae59d4a","timestamp":"2026-07-16T10:03:04.804Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning network-enabled RC testing**\n\n**Designing loopback multi-broker test setup**\n\n**Using drain_stream for state shaping**"},{"type":"thinking","thinking":"**Planning broker refresh signaling**\n\n**Diagnosing stream echo failures post-refresh**\n\n**Confirming causal echo failure pattern**"},{"type":"thinking","thinking":"**Clarifying controller connection roles**\n\n**Identifying test coverage gaps for dispatcher and brain restart**\n\n**Documenting debug rollout limitations**"},{"type":"thinking","thinking":"**Coordinating endpoint and lifecycle updates**"},{"type":"toolCall","id":"call_uj1ZurQNcTez4NrA1NrNzaUJ|fc_07547ad47f060e5d016a58ac54de90819aaed13e8e1e110e09","name":"irc","arguments":{"i":"Sharing dispatcher recovery root","op":"send","to":"RefreshLifecycle","message":"Found likely cross-layer root: brain refresh kills dispatcher workers; successor does rediscover net streams, but dispatch::peek_first_line(from_seq=0) depends on bounded StreamLog retaining opener. After 4096-chunk rollover, replay begins at retained floor (Input/Resize or partial NDJSON), classify_first_line=>Unknown, claimed prevents retry. Fresh rc stream restores. Please account for this in lifecycle conclusions.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6354,"output":1770,"cacheRead":118272,"cacheWrite":0,"totalTokens":126396,"reasoningTokens":1631,"cost":{"input":0.03177,"output":0.0531,"cacheRead":0.059135999999999994,"cacheWrite":0,"total":0.144006}},"stopReason":"toolUse","timestamp":1784196132203,"responseId":"resp_07547ad47f060e5d016a58ac24991c819aafdc952376673528","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ac28c35c819a85118a1b1d8bbbe3","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKw0carZUllymZrzc25ekSRZjMSeEiuU0CrejM-1zhRJZoK--WPLwTFhChzl8ZwbRYh5Bn7oQBZOygex86gtXz8vOAysWcA-RyUQO2lnkEhYUGj_xIbGfPiKdCwqMyXwg7DakiMJ5tVh10GIQw_UcCsa4VGhigwfNBwjjny8qqA019sCf_MjK4-lJbdRj-4rC28T6FtZD71WgTMsL9p8mjFp4Uu7HfNKg5DJ3nycR1FdFzQ_0tJYXdo7ezI6iEOgbtW9tdLbq5VuzNxREHbHBm43XzjrMgxlhNgpyGIu8iI8-XL7fNiTSFSBAvZOx9xgZR8e2xIx_lZUkJ9wcJ0vxxoFrOeqL_AM4GNhAIPSvN_yC6hHFuEV01l-oEvbJBysE38PPXdxZhLYVvWguuCEKsDboWFetUwSGJUyG7PfKEY-ZcpArP260W2u_A8Nkf1Zw9FfTTDcMcvdmO9aY4mMGhx0XW5V1breXo-BrF3U0FeVVCxCsPqi-PYqgCkmLc38nEmsp_pfx80mbFmFc0dbefp2t6ggdQFOpxxRV3T1FhHes-BqLrvuWL0GE6eLboz8qbI9GdPgmJ3G1KbjU8Qie9nq9JQ0vss1DuQdCIf3iGCDW6avh_sQPADhGZsM_WdVuv9CZTJfiKKnlt8RkqKvpUbztbyrhYTNFBqAkxI_yqA6sL9LsQgRuzmnTHqIMjb8d-_Dqe99I3S3BqdrdTPWhulUQttnaYpgHVCV_aEJ6Ob88ROS9Do11lmj6Iad89-8Bk-MIl46_Fmn6o6ZX4FJQME9LG5W3jmzFjFzG0JKUOMdMc24PM4UWxbhndu9ITL7KSeuQcBI-uju5r0_R9eGv3yf7s3soQLGD1xrVz0nvc34rd4iA73tdr-PH1xGD6W140CM-CRsOf8dvwFgMSkCtXAuBXBB6JJuUMDGMHH-3GVVmrAVREZy2bKx_fLagvRdp2SjhaHeN-za6m-pXWnundS4UyrctOoC6D__tXsvcShrnhcQyzwvsx2xuEqzSFAPyxCT0hnjzsu7Occ5SMOrNU2isJcYFEKTCOY2xmazQ7Ddwpc8oqWk9C2t4NLUhyXMLQBt0ePAZS5cBZ7TflP2vX2KoSejx72Hex2e0P49_9oDJ6JjEeWSImxp2ktQDOZkfjRlh6TSVCevybzwAVdg8kvfB39GWU6xNkL_en209kMUhHvVhNCs4hfL8OKnG78CChyeKybcq7KR78lvJ6-oHLX57kYv20GCikkImt5j9KAOHCtyxAoxTZWM2zNDVMtIvRPL5p4jbiAaUqYpyYbQvO3KINDpT2ICi4wANmX_YH8D5XcGDswOskdGb19YzNd9DZY77spSnmgHAmQG7OvHmUhiH7RHLOaYP6aZNuRjfmFnDF1RbczocSvk0gCxV7odlbifecWIkOS3tlCrerhC5RLKZ_GwwIeKZb81UAQBhEZG8V4cflDG-gV7FvXDlWY3rgnBv6Cm0DIYR9YVb1xDc-oxe77p5CZTERjtZFQKJK6gp7Tsxve3EcEjYOeLo6he8libRIDTp6we-VfFM1k9zWiwswG7mHSzZPSZMyA7Owv_mn25MDuEA1qVEHsxDhqrszqfrVyXlgOzXHjbiH09RKaxfwPNnX0l6YfDmz_irPzbrEggoJW-wf7bVKSVgmcXf8ES4UmPueDkWFGXfddupaEIKJhWFFUjLO_hX6C0i9Jg8G_kMl6pme5EEl2-nG40WmTLSOHg9nqEajnae5J0g0XGb_QOC3eGvOILrg54YCCF05fQ5BnDyh9yJmd-vKZ5BXZUc4sVGW7RYTjzAMWY6MYdY5Layo62J5chQNksPxfnjCnQYWCgF4Svs3TVokAvvgvhEBxNLNgiWJwGIg7jv3Tx1HteeKcoN5sICtD15KveSydfTQ8Ts1kYeRDgwYmuk2l5PIg55J1gzPhceUOBJ_H5FNSPyhDfty-whfPdWNGFzSaTchoLkU0BHFjNqYh_n5Ew4sOIohn3zVxFgsF9x4NA9Csb9lHmAUlY5V5JiqBBCJU4VbV3e3z6eGs0FYWKJPQZI76j3dbdNmZAQneShdZwT_tNxHqSGB_ss8BaOEK04-V7Fzk0A0shLttmyBPCrxv0na6Xbeboq7ly_sZRUYiMd3eDtXYGOZpKlPHRu1Y24EbpPWq-6a66_nwCkp1tyHlg6-bw9lvSIfeGGwlyrM6FTlX_in03RzcP6P9DdiOrBy85oIRl2nGiAnC0Jt4T8tB-19HX1ODZwyZJIX6x6Fb_M5AU_oiLF6ffjPC_Jv1MJi76WLnqhsaqGrO9XtoA41_st3uK7TpDBQXVk7mn0VyZKDQrTaaL_gQhi3aev8fDDSPL_PCg5rH1w3UoePfsz3N8KfC3kpNO3OFTXjgQgVmXgFpwAg5i1cEncV2Fg7-o1k21WQnn49WfooEUR-YgQgeS23TsxU09yv1G72mOwtbPkaYEmZ4Ye1Nb3nAWueqFO-6N4rmLb0jyVbv2ZpqdZN15hnefjjk1xY6oSeSs3gpMDL_REoqaUaXEs7wpdxBr3BhCCk4jxADaGKlyiuYQ8lOz3_wursvOi7uRWKhBuXq6XqQmlLFLequNZiaOFXwJN7fdvljK3DrAnwhJYlIsqKVtQeKMpntbQ09trQp14ucb6MrJzTaSSkxreocnlalG3wJf_Ixge2oVjzG2IutvNWazzFAe4aB8JCBUSBEjqfM1RQhL9ZeAaBVWZLWVxToPmOiS1OcdZS6b_O8WY_oO4ICUnD6zHVNFrkQ2h7tRj6y52EoQWGqUpGueZ6tWuEyfRdLqLNzb1OsAeL1vX5mi8KwyMKZXs3IXCyxddkRTeSs4gNUThDKlGCXfBt1u49s169LnoItPJr_5ghXMySCwWE7Qg0uFL-5WW__kzT6mfp1WflYgZpi5YpxG712LBafQOTYr15Im6l1c5oCaQc-IlEuez2qVxlaETbQXIU58TJ4SyelNHVGB8pcZut-ZYT83Ja6M3b9KPNW9IakDPb2Z7hiVDZhjX9lpPUNO-T0YvELr8GmTDCFdNVMUaOjsIfJfJraEkybiVEgCH2AHSd5SDyCrGKHBelOXGWzkoM_oboSMiShub0hga8QcALuSg2OQJqnQmSYrzR7SMaKTsHQjAZtTs6RN_DQlCiTHlYSPlYBVoJ52JRb52A_i7qq_XNZMP7miY8uuz2lo5Fe57AJw7slop_Y4CD8t4tdIx_XtYLZSWf8SHeRcTQir8MD0qdCdMcKOGT9xlTJ5bqNX1Y1_LdI3lQ3-kA-vR8-Hd_qSiDf_JjYF_2OriT5x704z6bilOIKKuoizIUJB1u-3DeVodXT4iijzgJwZ0xTQUUse8ddgLVyYjgJbtH7Yc2ewhiP4i6sBUt413P-QWAHE4HVP3v6FugCJyJvVv9r7fE4v7q28rsPcGY62kuCO2rbwBlRgsxADeGhUwG5e93kxJfiPVHsmo0nQwEcH4fbvTuJZsQgW3NyZp2oasowBW8v0xh0iAiyT1bmTyEvbs_U_S0RM-6q4GOWfzSreOZAMjCChUNSNtgd0Iz-oPKsTeugecClOsAYSoGBsy0MDAks8aJGlhVOC_aQ3O6dkPGXkPQxRsHjXOHUnNJKJqjKYE3y4IR_Nsm5y79aNslw9EzlvUiAe_u1FLvNpBRM5NK9a20LW_Jtu0j1DZOd3nVAPKD_9dzn0Mxglhf917SBhbgFvPVha7VjbmLev6QYkkuWtGlJ6ToV1JtrogrqdrmZrViGD5He7MpcP0RNtJ87Zd1KiqsydYg8piqRHFBdtKxWED7mIjoogeHwC0BI_DNQrwzrXZNnmmz5GfvBIYcsOpBS5O8cF2IYOk8dRgrtB-1YZ3h9abX4KgAO3FGxXJECXnU9JpQ8lHvnK3QVG5nrdKiDUR4XbEScH09EXufMhv3A3dHV0E_ejU1sA0WpL1BXPMnEth4TngVfNK65mgul6MtzY58Zi9UYPyE0Sfa3wkTiaEsPP59Dzyk3_9M0uDOYbTghx6JoJDe9fyFHzRuF6ByJ8KvTTW30Y","summary":[{"type":"summary_text","text":"**Planning network-enabled RC testing**"},{"type":"summary_text","text":"**Designing loopback multi-broker test setup**"},{"type":"summary_text","text":"**Using drain_stream for state shaping**"}]},{"id":"rs_07547ad47f060e5d016a58ac34c524819aa98f1f169c868e7a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKxDKIykoEA2Srm882puDwz3LQUtZWzCq2JKW4rXsh5trfusnX6xQjVE_5yPz26Ioas1sTG1tkHeyjXg5KKVK3aiDsMnBX3FGvm8a-VIaXIRhnfJ32_166Bn_aMpSYfDZTjyy7NSpjeWwbDKCyRJ4uwPvKYLZJz_soySYduC8o13XFcskzMdivSXgysLZuKH8QSqciyKYn0Rat1U6kLCg7xrgh3nTRgmqPjjRAmdx5-V04hiJeRgFZyhkFzRN32XrUnAGOISmk522Y7mJg3oGXdjtzt2kMZLIAc8ELzu8VfuRb2Vitu-I1shFQsO3N81Z23hlLzvSD4BW0PagxWrVB7U54SzIps1HJXrqsmbMMZpt6cTSl7eWKJTMrz6froTzbQqhvHxy6kvPehtJwEMRS64dHWL5pJ1qWJqd5QFmF0QmM24uYg8R9O4-Y71p-8hSzo6qPnER2d76V57I_NVlB-JGFRz4z_XA05Kmsk3-SM68Ylh3dm7na2j1g3I0ynsZBMDIfQxoZXWWlYwvsoWm1penQTXKsFEIEZmeouL_4f1qzp2eM_Oa7JSV48Ru5YXLg7nAR0x8BxQ8ziwDcKodyGFnOCS4mcr_W1exEnnkMeHJzihaItJWPw4F9bk5X2grsvH5VVzzjuPlVhG75xFAffToMljPYqSKPbZFUqPy60fy-br0CJlgS-bAfxGvGmFVpS3oi5xYS2s-IGOHO4C_JNuG8ua0oHHffxfJzeQlCRxho--UEl4oSdFbtUMRyNPah_jBxJTg7RipNXk1j2_EfWZHLYY13r3i6S_YowXnuaFipYbiSRUh6MP7mh28s03Ya7vQNLy9prJfRyyD2dhjBAthLe5RX88FDPzcg2oLPT14BqC_MyuSdgMnpw6189HA2oc7Fj746mJzz6Db09NoSQ0SeX4p7LJoLyq_YXMfIaYMt2bw5WJtdALwTHNKWJEzdGn1FZGTEave0LPSnJAecPAft6KDZ6t_kV_8ktJ6v135OI28qdw3H8cCgOIOOTqJTwm775iMKYFdz2Cq3rTuxdtikGDRzMj9cCDQWBUE7NgjBgEKw3BEEDkVXC0Mz1_3NuVJkGdRfWxu6wGTDCU6gD7dlL_SVH9TlFDUZ4Tdrl-fbI0yubENvdJNtpiUcuqXZjGSE5MGcOp_e16YG26kSQ78lOCcQmVo3fr9oDlCM5iIxAQ-xMarcop9XXJy8zt8p5I4HEi-RCevEsSXFqRQULUfTt7tJaJ4pr0OBRpHXuz4dAAL5rLB2LLu7xRrLVYodrarVTkFnL6gt7bD2Eg5KC1naxqXFh3Kq4Dgoe_uEKL5pRoeifvchFxOKdiDNc4xt719KscFW04xxb3YI38tUpQPKR3ZGnFti59Lv7NMizfIQgjzL1nGKCzxqarNG-hPqt5sBExzTK4IEnWuh0tPpbWmHzmCMRSmcN02uPxX9kkBsMcezVRYxWxCgsaXlw-tXwlISRUnPoSkU1F2XiZPWoVMSf8pIH_SmY5udppF5w7kDTHPAvlPajernIqWfLe9oO_xKrpzGOs8KDlv8bm1cpmKdMZXDNVAUUn9KGyd8o46HzY98zX4aqVr-4tqlKGuYucu__nkn81cHqABhiGH7IrkQ3l1VuQ3Odf4LEBXWAxpbJThFcOGFIc3Pt49Igz5vIV5oUKB4yBmPyFRxRFr4-8PMTT-PMNp0FW5rwzdAPZR0usGti9Uc3s_WwaanPxpJfxKdfml1PbQz3MmNgs5vSm-OksbJZO0gPSNRHvQyptFdV1Q98uA_tdUYwv1dwatSnCOLQfiG7reaEyI_jW-S1jUC5sDjrYRVQvMxvR9V5cyx3AiD0fdptKcB8beV1tUDmlqCmfi0gdLLD9IruJn44SPlqT6Z8I9LCgnHptKWuhVDP5ywYXqH6CSQLpRHRAUK6NWGDAEM5uFOlUvDMgOrSgQChNqcTY8xxAn-KkCaHdz4WymqF_h3UBmacjMXSklhECebcYVM8RTZY9x8h_CekymSqf2iIO_9fHp-hqg-FCfVHwuwoCMzsixXI1U7URYIRk169OQ6emFN35dGu1Ru5sU-_gIVj6FWjZ1f-IGdY-NaEz-hoVjsqruvl8f1l9ZPtP6-nkC3jMfhX88z0_4i8AODtrxvd2i7Kio3beQ4iaDQ7d3POGE1ojTACA0RgZES7n1g2fuHMMrzUt8eR0IzzcORW7-pkZIqJYmXfX7kRDIsGrxMabUfqK0lzs9-Nd7vNusyl89oEtBxFSPYHGnv-3uH3_n5Qau116xa8wcaIXejLL3M3UaeBeB9jEUqDQvYbdYFD3pzo9IqMfnqSBTIBUHrDF0fTyQSa92eP_7kBYRaSRTEyQDw-lVc9GqdHUBBCn0MiS9QpS3hfZHxpTMXJTSpuStUVOPz0q73yGtcGWj-HDau4-8_QaO__ekiBL0gfl2Cl6Uq8uMrET5ZHzmoj15vkxyHkKZ9hNTNzCuwCeyc4iyEE4Da2FTtCgZJHy7CNmgR30rv4wIRnAMuTJoeP1rfySqBZmWXBbdIZi-X1rpyeKrGn8E1zi4pA-0ZJdyawUU3muG8MWFsYh5J9Hn1J189FhtUSTpO4QaqQJpnAf6xj6uSTV_C--wD096GEoHsUni3fKkz9DiUazz5vfKtXLdV4OdDSzFD89mpnrBIf9kgHUmc6EbH6A9C1EE6LLQ-qadVpw881E0S8Hmb5_Tsujg59lugvi5PjFHxyRkaqPoJvprtEXuNsQLv4tLwgfAoNEYDy-i1nmDELsklL9009GwqToDrK79HUWvzCgF2TcyZ2yAUtYaGCLkkgq-K4--k6-VOBBFq73qKZSO29hw54ATLtoSHA-UDuE3C2lavLbIX3whi8fBuzdwbF26Ajh5xHxAhBRM2Zwky9jtjnnlLgQekIIrHffVQU1vxMCpt8mmv1t-2HT0_axI_Col9JZ1thjWJ4Pfv0I61DfpI67-EQi42-fwNj9jsG5Lw33iTysIG_AKdm_qFiN3a1pxcJvCH48Z98Fp7vB7Grw3SL032BbU25dwNTYlHL9As7CoMWhsk4bBzr-6m8yekl-4RkwydRccGNk0xmFhEM8HzWlz_S9ftXGfdNeJdnHLj4-ro6-d_SqqEtU_k9NM42KIGnoO7uia_keLfZ3Cg0-ZwmJLlyCZWUmOZRsqW1HkTpU7zo_0sSvj3yGqms-Mq0q4voEYmmV4XgPL7KA-3JbkwySuaTKksPeORRSX5XP-2sAEMAy0BLO9ZSnm90XrF7KI0tgnkjVSYxpjnx61R0T1TV8D9aMY4-zsr-0IY0iRc2SUxTT_qaGtY-gh_mEFUhFqHjS35EsdchHiiOpeg1o143t9P88jCCDjivf19PsBClym82cDWx1160P-rdzAch6wUBKtgGQ2aae0UAKpUalbBvSx_s4H9uISmNNLWMb2craRhVGBNXnQBFrCkWiHFs_kVTkn5c6lcjWeOa6hNIQsTqh6Z4AHNnafkWEz-lfJ79IhIX9JYVwFzfltwd_jPJS54B2RXdck8Ybn-_rHP5rVvnSt2AO8jl84aJ5gH8Gxqp5zZSIVXLUY2-kyw6YjFOtmlY097WxfPcZ93KjYVFKZECLzcvh5fAFvDEeuuYHbdvm5F7UDgU1pggeXz5tx_hDZn5wUEcEN6qQAFpub6S9bybMA3Bwmp8YvNA5xFegcSiETZHDNGrwvFxG3IrKLDWiCaDqTOPmXl_Omhn6bGbK1ndsEwQUi73kPVBzznBLEoZ1FdY4EkrTHBwIQICIhX990VgMBrmN3OK_puYk1NTIxKbG-F4pch97zW2Yoy3QtowBM7MdR2F52kpPsx3lDaGhHf6G1sAqzu7nB1PnCSqSd3y4iKFs7z_osKHGHgoi1hB4hRngok47DfIdp1e1wpQsahOLlyhanZ-97EjomBZKA1JpVU9kesRhvToh7eyJSlnzzupiXxJ78xnosYrz_OiYVDPxSRrVOeTJOaWyGo3ohT-xDaHF4UBx4Hi5_LPtfNoSSa_a9jTRsImgMIzBc6OPIMlsnSkzmBQ6y8O90YcENuih2jmqjIxQOI7INKVYFUJai4i_fX1gABzNO8wTeZtFyfIYcmbXXg7QcpMtFbcb8C5i0MlCRkh6pEP4oU3gE6Kkbsg-YO4=","summary":[{"type":"summary_text","text":"**Planning broker refresh signaling**"},{"type":"summary_text","text":"**Diagnosing stream echo failures post-refresh**"},{"type":"summary_text","text":"**Confirming causal echo failure pattern**"}]},{"id":"rs_07547ad47f060e5d016a58ac434a38819aacad43c22cf107da","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKxUeleGq3Vcgx64nAdh_NtVY5qjufKh80Z_nDdEPBfJbDp9NzuIKSC-rU-_QIaHYsK-7tGi_EQs-q6u8B3Dsbpg0XQ2UsZvZQfK6QxyQGSun1ntpZod-MU8cAH6KmexefA5V8KtmqswC7Z0rePldceVLUhotIzDNPcd9RBxczJ12MXl3uW9wugyN3IoD8QS-lXANXKYpV9uTiikE_0u620TgWT1J2WrmD3Mz8Hpjf4gmT1hjimdbJG6kh9G0_64HBZpo9IS7jMYy_s144ENEmtldJNMSQT78RFcFHluEWvOlpCylvE-AP8KbbsrnK7thCGGEpg5swNaA_2CxwR0phsMyTjuqMBCZkPY-2XGYwTaIg76ClTnX2OOn74IRyEMwSaJh2MJA8jl6V796BZNz7TnQ_IpW5Po2O5yMAa1UqANPI8x_JNgOKNgfktg_rrXjoJpCww2I0DUKNhK-feuhVPPoj-GnNls3WtaxzO2uQLVXm5uubtkegz5IH5xUCj4Mf9srkrLxBiWm1gcBbWfCBzya5jDFwc1bXP9PZ9GuW7qsKABW0WEl6S3AH3ZAAXWoXt7WJj4QMHmsI3axaYMBsCgzjpG-5kYjqbtTBnMHk1QvcxORzNr7sMhTAXhQl1Ugg-B3qlN34XoO8_QqDY7rNB9UAf_HsBqb2aqBP_QSyrAOugLPVKbAEYMU6E78wgbxX3z9nnqEEKeXt-zWQsjuUrNUDISF0SbjZ7hewnpOrayRWWJdS5SvSoFYuuw__5N0QoT-tKk3qOtENRPLITbVbN0lZZeYVo4otM7zxzHsjWllVer4lLPMAgr7vM-_eS8qEXLt3BnETKcOVPNLKNHuoc_6MC-s1kQTLVxJMDvv-PWvBFNnB2iTo5X6fIK4rWOwbI5CG8E2eOf6YM5vuwlE_dRwHceB0I2TllKONlcokWjpEzJcrR4YKiZqqy6xQM5NcBX9_a5GgStN1_MwQPiYVJHaIUT4S4qF6tHnXel39zgpaxeCvKknJ2Z6Hoihrb7-tx7p2Ff36Q7e9pl_LtaT1iDVp1D108WhklR2ruSLcYOLuqnr7LHU0cCwiEXbOc6g9cPxtT2Zs06NmGnrrdBpxHf3C6OxbRwe3ttboi1-8C8e6N4qJMDeKrwpmijcvwbE_CLmCUFS_Fb2PYqtigOXV3BGyzAM6r6_BFGfJWfxzC_iTm2OF-PXtpsfaQEjp1UHpm1yV246MqGKGTxeRN4llhayCa5EXO6G4IDXvrzHiFjWE4AtEfh4-B_WAGhy1qrfj0NEoVWcdOqDThbUyQOakXK010rr6ZGPictetwsSeD-7RWaTOkkkoX2cXZryJ1rZxoZ1Fa5rtppmGoJ-JCx9kyW_UsyGlU_mwtaopGSGK5RH02MCmDTQwDjPdOQbUFk4iQ2Jeyo-GxKmp-VC8ZZ5VyAa8wAWkCjl3wUkDBxg49ufU0TtNVVMqFhf0255PNtp-gWBZyJcohwO5mA7iVQlAO_eSG8G1PO6UGwSzuvwaA6OeLAHwD4gHy0bffXw5MXtqKmDmTO3sWkSUgQoRbTl5ZJUvK_1ehVxWRWFYdS0vxLln78aVhypXSkAHYSS9cro02aGGLq6vwJyLz2EOXhN7xtZlbJVdWh2hR8eFPjzz7L5hcTOtRPi0vun0MgGaNJoN-erzJ9B49aleDT5SIiYBsLPRTe3qEtge_61G5CEs7JZsqwFS9cyUf3-xdZ1gz-PuF_mT0H5gVMX4ZNrELNPEFJS5OyR7_A9_rW-MhLtUS4AirM6rG5UVQ7B2Hx9sriOTPqEsG2EX3NmQWjIQ1fm8CBbaFmZOsVZvWabHYDkbfdvQpy3YKg4a49dCgqRbZwsJ8fjGaoIHhsy-60tOwusS3EqcYswQxfkUEr_38PQaplMVwRZnkNDMuY_rr9sn46E4LQnawcIaJLyajBLzH76SOsi5BWxKg_tD3jbiGJF0xFds_BoMaJD10dmBcdecDtzRu4JueAYejmsZCshM_XTS_AVq2GvDT2Mi7VftGuinjc5TSGtivGIY8cLR30lvnJW7IbwQlMSrNZxxQrywLIW_I8U96gCAVRba9hVhzf0fzrumpCwyHKR4z1sQXimAs58FpP0KXDKhlv5ISjbf1h4FLUCiBcDQvbATV1O-UbzzG6ZptSIdmswRm7FTUzvHH80O5JwGhzRkiBcdPP5F5ipsPweEOopfKJa6ki0V_sLph-fU_L9opzteV3PpwsDdIxTW9SF3SAOPXRO7rpl4xG7FAvzsY04tJv7CmaHn_x6LjpKkJeZyqeLElby3HDO-LDM53TbajoJuzJDIdwxgFY3-8MD0tdbUSUM1TXBkqq-r9-S7Sv8BalWklKcG3niPiDHR8uLwEYTzAB0e33lo03y_kRhaIa2_Al5qVAU05ZTVFBOP_mIqB-oBsOWLvnMihfJLFYXCUmRo_rXMmknn8-g7gK-qQTJ72Xly757YieKjY4TTCxkCS9-cjn31Siy3IMjKqkLyPBBfwbwnKEqNdIPuIP6znDzE5Cv210y5WZAhJ0CTmm_V6dPLuJxp4wFM3r0DAFnQOCNxZknlOlktA0EHweo4LBreewHlAOC79_R0XNX-ADy-lfyd5mDHjLKzu1_IBIEBjFjmrmuCpTNbbXnBy2CnvAZZ-4KT3jxtEcALZl_EkvcZPFJf90Rgyx2CY08tvuO_ex3MxxaR_zg6ZkZIO74MSTC3n75YHdumRULCNoq7wv0CZzQ1xeW5nA44RDaNVTOjIyDDnQTBaWLrsXr7_KwAFIIMRcj6dmOC_eUZ9dyQ9PaLtw63sL_HE2U2_yMFhI3hljGI_jC79Gx7QLPGLtIe4wmmXMYIhaj9TjCuq9UbqZ-VILOR2lX-KSXAkWvmz6cZDtsz8vC5kyqjm7bzD0xsqXR1oNp6Cg7ORmykkE5RqBgy_lt57nXLzZT1rVORFNr3ZPg_qtXgNjn7-r-0G2u-ZnzeEuCqhs05Q4xO_S6H3qddnkG48eWNG2l-YfRYHtUo1JZRGs9Bq_iGL34Mz-jxc6PzMlZKaLWFhqXL3pta2re6xXUgsuO2ZgWWQNil9z8NKU6e-crfSG9HL3YSWpyBUShgHKwvkhDTNC087bfKBeC2KrghQHRjtUrL0Dzz0nNr8D8eoZGOUAUzX2KfjmZvBaVItJ6-r6Pkf1DD5f0SdBB7p90_KHhXdwqPIhfa2EQ1ASgOZY-flUd99nAorqrl50y_OSfnu7UCg4aqmX5ur0FwJwpj8Npu5QD4EWVfCWzQbGOKK8uTuaWENfw14u05fMyvxRe9v9IG45zfJ-xj6cYnn4weig3e3vhEiAbPwL_czk5khM3gt9p_YdA-TQ-zWz_Q1IfxcVgZS4dL42upu9fZqQAEPsGCxG3HXNFo2Y1XPpkez1ygJQC8c-9dpzm0GV-ksdRRyWTr60SHryVCaRXR416K6qdSA7RsufW98KELhxpF5ITCqB1-zBp2O1zyZJ503qj91vKTQ54PMd0EP-sSwmAncuzJtthay3opG7Tr-xp6aLEQif-3DPgKq7tCKv5AGua1j7GaHlBIeEQZkCFP_bvIzwbzXkJaxGMGt0CvD9P-Fd9zl9sTeB_pPCDXNVrF6gDSyORb1yNIwuey-gDO2kUYocMVdjHVJsM840ujVIIHnpuiDZDDopSFHGUbRYJzNcjggtzKiTdwI1hu2Lg0HEe6WIs9bnnZz8przO8rs1Z_zynB1zXoV32LhmxsBWCYtR3jvVWYgxKNFDDwPcCL-n1Zm3x1ZLaGWsF3vBkPoS6-KH9TqyI9rYBBWnvJ2PNNdLtc4tWTZj_S_CbwHz1f_APcQBatB0PBgteKgiYQpX7sXOhYHvE9UkNZyGx3RYPXoSe9hRhR4FCJQSo_iSsy15bvA-g0f3F7e7RbHaP_SEOG-ShtJKO6ZQuRhGaug4KE_-rReZjJqd95kYa6sAThAO1KhbEEwQS89ay17WcjnUVGkntdCHcup2F1J8gNpcDx1Y3U1PQw4kikd_jsQ=","summary":[{"type":"summary_text","text":"**Clarifying controller connection roles**"},{"type":"summary_text","text":"**Identifying test coverage gaps for dispatcher and brain restart**"},{"type":"summary_text","text":"**Documenting debug rollout limitations**"}]},{"id":"rs_07547ad47f060e5d016a58ac541998819a80047f4f680d38d8","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKxUTq6DrUT-mgGDznsQGkAVKv0PNin5H4vi8Hrv5LTlfqxWGvmT5PYK2Uv4xj_z9Ab2UVob4iQzhWxDJlJs_MdoFyoUpxdTz70walfemYTTRcfacm45jAYsHKmIqCBjBxo_aIXRZ1oIxD_gakV05COMDoNAAe03dp5rYmahRhR-DaYLBdmRSKvnY_FvvD0jjXWd-mc6G8rtx7KrTkniT6e_rpFhmpJ6j7_ePEQ0tB79hx12bVsHb4V3rm8QSv74lwINbbLpVWZyFy4rclJEjlgA5NnCGx8dU72Nu4ldyOeaXiFsigDkYB28byp8lzBSa1Ov-YA2jrAg7Kf0ciiZc9p-Yo34JWAL8bDff0_3HhO8swv4Gn8AtnrJ5XAZnnG9Ho_xGyQ1t__kmUc5qIS_muECi24QRl_mLyGz-x_MeuCEydiHNjHyTF7Qniwoy9gyY4Zrws6oHYnZBcR1bZuW2wvQDFQsI3N5NZppIwF2fD419SiBYmcRsnCMhqr9Q42Jfxf6sWvQWEnhOwSd3g7FOL-j4iHcplVmTRCcIaZcEW4hKF_nxHp-nO0cjVGp2JchNek-HvOA84f-qJ5bjruh-OXnG46L7VJfefhzPCmjffMQqKrQyIIBLvM71Gg-ZGwR3LeBdQYI7w07h5w7G3k04RXr9PCyJ8auS6JPtwFIN4ukLyIlva8sYEnRFn92ypqiry73IlnXs1JE3iMMJNV_OUn6VYhrsS1Gw7NijODW1flZCRJMyflX6ym1WuoEYCz6C3D1MkElK7vnXzSXQDWza_NJFcgwnsqU-isEPWGlgObPSjnPbIZic9aGsrboegVhKdCp8rLKrH_H3_TQ5_n3Sm86Yvr3LwhYlJkwpLfgzm9HS4qVr3eWoKkI2IQHWdv1fH8M0bJBV1VIAsvggWOfnVQit0pLT5B99FRmAWUyoY9W5cnBEN-4ggjt0omY3arUd5jHGohSAJgDFQsT4d6oytAorCIqmDcTR_SHYlWML3G01V90DM5ZA7tcWLhysNQVR7OEzMVHVZLkwpQpjF_gHNtGDSMcl5DMX4dhzGvYB7GKX504Ff5tovMJTybez_8zWgtBokOvbBak0x2okjNEBK_74UzeFZQMDM7nK7Xcvaq_U9BK0VE39H_RcFhAimtpQ4KJXQ_QwaofLnw23GYaq2yvDsflEADFifiTAl1mkumktTlRoatY9I4YJrcqnAsjvR3MEoRxxRHy-6klWrWFHeiSlfDdc2Le4EgzuViz9YCduULJmn-JuuywZt5tqg1NwM_rATaDcQwXFvWMf-0P4PKAulG1vRbI_RX3rcZ26ZQI010tmjLXpnpTHdlg_uY0IJL_u3G1OnLjhukHgg3AWsALafvopJCDXRgk2A4gsib4sOJlkI2WpB06EleZy9YHS4qGGcGZanYXDYniTUICr1ntOg==","summary":[{"type":"summary_text","text":"**Coordinating endpoint and lifecycle updates**"}]},{"id":"fc_07547ad47f060e5d016a58ac54de90819aaed13e8e1e110e09","type":"function_call","status":"completed","arguments":"{\"i\":\"Sharing dispatcher recovery root\",\"op\":\"send\",\"to\":\"RefreshLifecycle\",\"message\":\"Found likely cross-layer root: brain refresh kills dispatcher workers; successor does rediscover net streams, but dispatch::peek_first_line(from_seq=0) depends on bounded StreamLog retaining opener. After 4096-chunk rollover, replay begins at retained floor (Input/Resize or partial NDJSON), classify_first_line=>Unknown, claimed prevents retry. Fresh rc stream restores. Please account for this in lifecycle conclusions.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_uj1ZurQNcTez4NrA1NrNzaUJ","name":"irc"}]},"duration":52599.49230000004,"ttft":4538.402400000021,"contextSnapshot":{"promptTokens":124626,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_uj1ZurQNcTez4NrA1NrNzaUJ|fc_07547ad47f060e5d016a58ac54de90819aaed13e8e1e110e09","toolName":"irc","startedAt":"2026-07-16T10:03:04.805Z","intent":"Sharing dispatcher recovery root"},"id":"4185d3ba","parentId":"3a56899c","timestamp":"2026-07-16T10:03:04.805Z"}
{"type":"message","id":"82dda2f0","parentId":"4185d3ba","timestamp":"2026-07-16T10:03:04.805Z","message":{"role":"toolResult","toolCallId":"call_uj1ZurQNcTez4NrA1NrNzaUJ|fc_07547ad47f060e5d016a58ac54de90819aaed13e8e1e110e09","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- RefreshLifecycle: injected"}],"details":{"op":"send","from":"ReproHarness","to":"RefreshLifecycle","receipts":[{"to":"RefreshLifecycle","outcome":"injected"}]},"isError":false,"timestamp":1784196184805}}
{"type":"message","id":"87a346f0","parentId":"82dda2f0","timestamp":"2026-07-16T10:03:04.806Z","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":"760dc594","parentId":"87a346f0","timestamp":"2026-07-16T10:05:13.870Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Summarizing test findings and architecture details**\n\n**Detailing causal inference and feedback loop architecture**\n\n**Confirming finished stream redispatch race condition**"},{"type":"thinking","thinking":"**Planning tests for stale stream races**\n\n**Designing two-phase stale stream tests**\n\n**Confirming no tests executed**"},{"type":"thinking","thinking":"**Reviewing nextest test commands**"},{"type":"toolCall","id":"call_iJY9LfYipMD9Hdk0bcwfQVKX|fc_07547ad47f060e5d016a58ac7e8bd8819abb76990555258fbf","name":"yield","arguments":{"type":null,"result":{"data":{"summary":"Investigation complete, read-only. No refresh/update was invoked, no live endpoint was touched, and no tests/builds were run, per the assignment constraints.\n\nCLOSEST EXISTING HARNESSES\n\n1. `crates/spt/tests/daemon_refresh_e2e.rs::daemon_refresh_cycles_brain_while_hosted_endpoint_survives` is the closest full production-process refresh harness. It creates a temporary `SPT_HOME`, installs a private mock adapter, launches the real `spt daemon run` broker/supervisor process, starts a real broker-hosted dummy-harness PTY, invokes the real `spt daemon refresh` CLI, and proves brain PID/generation changed while broker PID, harness PID, and ONLINE status survived. Command: `cargo nextest run -p spt --test daemon_refresh_e2e`. Critical limitation: it deliberately writes an invalid `identity/node.key` to make the rig NET-LESS, starts only one endpoint, never starts `spt rc` or any attach stream, never injects PTY input, and never reads post-refresh PTY output. It therefore cannot detect loss/replay of dispatcher-owned controller streams.\n\n2. `crates/spt/tests/resume_no_control_steal_e2e.rs::brain_respawn_keeps_every_session_controller_and_still_promotes` has the best existing N-controller brain-replacement rig. It runs a real `spt daemon brain` child under `supervise_brain`, an in-process real broker/NetHost, and N real ticking PTYs, with one raw local controller connection per PTY. It proves every held controller continues receiving output across a real brain-process replacement. Command: `cargo nextest run -p spt --test resume_no_control_steal_e2e`. Limitation: these are broker `KIND_SPAWN` auto-controller connections, not remote/local `spt rc` NetStreams; it asserts output only, not operator-input→PTY→output round trips; it does not run the `daemon refresh` CLI.\n\n3. `crates/spt-daemon/tests/attach.rs::attach_survives_target_brain_restart_exactly_once` is the strongest existing bidirectional attach-restart proof. Two real loopback NetHosts/brokers host a real echo PTY; an operator sends `MARKER_ONE`, the serving brain dies, `MARKER_TWO` is sent in the dead window, and a successor re-serves the same broker-held stream. It asserts both markers on the rendered viewport and source PTY screen. Focused command: `cargo nextest run -p spt-daemon --test attach -E 'test(attach_survives_target_brain_restart_exactly_once)'`. Critical limitation: the test already knows the stream family/id and manually calls `serve_attach` on the successor. It bypasses production `run_dispatch_loop` rediscovery and `peek_first_line` classification, which is exactly where refresh recovery can fail.\n\n4. `crates/spt-daemon/tests/dispatch.rs::dispatcher_serves_a_remote_drive_attach_undriven` and `dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint` exercise the production dispatcher with real brokers, NetHosts, echo PTYs, attach requests, controller input, and echoed output. Focused commands: `cargo nextest run -p spt-daemon --test dispatch -E 'test(dispatcher_serves_a_remote_drive_attach_undriven)'` and `cargo nextest run -p spt-daemon --test dispatch -E 'test(dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint)'`. Limitation: the dispatcher is started once; neither test kills/replaces it, cycles a brain, fills/evicts a stream ring, or leaves stale finished attach streams for a successor dispatcher.\n\n5. Adjacent evidence: `crates/spt/tests/attach_wedge_e2e.rs` is the closest real detached-daemon + real `spt rc` process rig, but tests abrupt rc/PTY death and subsequent new attach, not refresh; `crates/spt/tests/brain_respawn_rename.rs` tests a real daemon/binary rename/brain respawn but has no PTY/controller; `crates/spt/tests/brain_survive.rs` tests process-level survival across swapped brain bytes but not production attach redispatch; `crates/spt-daemon/tests/brain_resume_conn_deadlock.rs`, `brain_decouple.rs`, and `conn_blackhole_lifecycle.rs` cover non-draining subscriber, connection retirement, and unrelated-session liveness but not the remote attach dispatcher lifecycle.\n\nCONFIRMED CALL CHAIN AND OWNERSHIP\n\n- PTY data plane: `Broker` owns each `PtySession`, input writer, output ring, controller/viewer table, child PID, and effect journal. `broker.rs::handle_conn` explicitly detaches a dead subscriber but leaves sessions running; NetHost likewise detaches a dead stream subscriber while preserving streams/rings.\n- Controller/viewer connection: `serve_attach` owns the serving side of an rc viewport. Its primary `Brain` connection subscribes both to the peer-initiated NetStream and to the PTY as controller/viewer; a second `wire` Brain connection forwards session output back onto the NetStream to avoid same-connection replay deadlock. Controller input is decoded from `AttachRecord::Input` and delivered with `send_effect_no_ack`; output is encoded as `AttachRecord::Output`.\n- Daemon brain refresh: `cmd_daemon_refresh` calls `refresh_brain`; the supervisor cycles only the brain child. `run_brain` invokes cursor-only `Brain::resume_session_cursors`, intentionally making no PTY subscriptions because the main heartbeat connection is non-draining. Thus `BRAIN_RESUMED` logs session cursors, not rc controller streams. This is correct and orthogonal to rc restoration.\n- NetStream/rc restoration: after `run_brain` reaches its heartbeat, `spawn_net_consumers` starts a fresh dispatcher. `run_dispatch_loop` calls `Brain::net_streams`, selects peer-initiated streams, and spawns `worker`; `worker` calls `peek_first_line(stream_id, from_seq=0)`, classifies the protocol, then creates a fresh Brain and calls `serve_attach`. `serve_attach` replays the Request, resolves endpoint→session, and `attach_as(..., Some(origin_node))` silently retakes the same-origin controller. Therefore the architecture does contain an automatic rc re-serve path; saying the successor only restores `BRAIN_RESUMED` cursors omits this separate dispatcher reconstruction path.\n- Broker lifecycle: a brain-only refresh must not change broker PID, PTY PID, NetHost connection, or NetStream table. A broker restart is a separate lifecycle and severs these ephemeral controller transports; it must not be conflated with brain refresh.\n\nCAUSAL CANDIDATES\n\nA. Stale finished-stream redispatch/takeover — primary candidate, `[INFERENCE]`. `NetStreamInfo` exposes `finished`, but `run_dispatch_loop` selects on only `!initiated_locally`, `next_seq > 0`, and fresh `claimed` membership; it does not exclude finished streams. Every brain generation starts with an empty `claimed` set. Consequently a successor can redispatch historic, already-finished attach streams, replay their old Request records, and transiently/silently retake a same-origin controller while active streams are also being reconstructed. Multiple historical streams can race in HashMap iteration/worker scheduling. This is a direct candidate for partial session recovery and controller churn.\n\nB. Evicted opener makes redispatch one-shot fail — structurally confirmed, causal application `[INFERENCE]`. `NetHost::StreamLog` is a bounded ordinary ring of 4,096 transport-read chunks and evicts its oldest chunks. `run_dispatch_loop` nevertheless reconstructs protocol family exclusively by subscribing from logical zero and parsing the first retained NDJSON line. If the original attach `Request` has rolled out—or the retained floor begins in the middle of an NDJSON record—the first parsable retained record is typically `Input`/`Resize` or invalid JSON. `classify_first_line` deliberately returns `Unknown` for mid-protocol records. The worker exits, but the dispatcher has already inserted the stream ID into `claimed`, so that generation never retries it. Detach+reattach creates a new stream whose Request is again at its retained head, explaining immediate recovery.\n\nC. Partial 4/5 observation: `[INFERENCE]` both candidates naturally produce a non-uniform blast radius. Finished-stream history differs per endpoint, and opener retention depends on the amount/coalescing of operator→target traffic. Four older/high-input streams can have lost their Request or have more stale finished predecessors while one newer/quieter stream retains a classifiable opener and re-serves normally. This is more specific than an undifferentiated PTY freeze: PTY children and broker rings can remain alive while only selected controller forwarding workers fail to reconstruct.\n\nSMALLEST SAFE DETERMINISTIC FEEDBACK LOOP\n\nCompose the `resume_no_control_steal_e2e` real-brain supervisor with the `dispatch.rs` real remote-attach echo flow in a new isolated integration test (suggested name `refresh_rc_stream_recovery_e2e`; avoid `update`/`setup`/`install` in the test-binary name because Windows installer heuristics can cause UAC error 740).\n\nUse a temporary process-global `SPT_HOME`; one in-process target `Broker::bind_in_with_net` with loopback-only `NetHost`; one in-process operator broker; and the real `CARGO_BIN_EXE_spt` brain subprocess under `supervise_brain`. No discovery, relay, production socket, production perch, or live user process is involved. Wire the broker's `BrainRestart` signal exactly as the supervisor tests do, then invoke the private-home `spt daemon refresh` CLI if exact verb coverage is desired; alternatively call `BrainRestart::request` for the narrower brain-replacement loop.\n\nUse N=3 because it is the minimum that can express “most but not all”: spawn three labeled real echo PTYs (`cat` on Unix, `findstr .` on Windows); open three endpoint-addressed Control attach streams; send unique PRE markers and require all three to echo. Preserve the Request in stream 3. For streams 1 and 2, deterministically shape the broker-held peer ring into the natural post-rollover state by calling the test-held target `NetHost::drain_stream(peer_stream_id)`, assert the drained bytes contained the attach Request, then send/observe another input so the retained head is a mid-protocol Input record. This avoids 4,096 timing-sensitive writes or hundreds of MiB of artificial traffic while representing the same observable broker state as bounded-ring rollover.\n\nCycle the real brain and wait for a changed PID/generation plus `BRAIN_NET_CONSUMERS_UP`/a bounded dispatcher-ready condition—not merely `brain.ready`, because current `run_brain` writes ready before its first 500 ms heartbeat starts net consumers. Then send unique POST markers down all three existing controller streams and collect results with bounded `read_event_until` loops. Required pass contract after a fix: all 3 existing streams return their own POST marker exactly once; all PTY PIDs and the broker remain unchanged. Current discriminating failure: stream 3 (retained opener) recovers while streams 1 and 2 time out because successor classification sees Input rather than Request. After collecting that failure state, detach and create fresh attach streams for 1 and 2 and prove RECOVER markers echo; this verifies the reported detach+reattach recovery without masking the original failed assertion.\n\nAdd a separate deterministic selection test for candidate A: construct active and finished peer-initiated attach streams and assert successor claim selection excludes `finished=true` rows before any worker can replay an old Request. Do not rely on concurrent worker ordering to prove this invariant.\n\nThe single end-to-end pass/fail signal should be: `pre_echoes == 3 && broker_pid_same && pty_pids_same && brain_pid_changed && existing_post_echoes == 3`. Diagnostic output should include, per stream: stream ID, `finished`, `next_seq`, whether opener was retained/drained, pre/post echo, and fresh-reattach recovery. This separates PTY survival from controller-stream restoration and makes a failed run actionable.\n\nHISTORICAL RULES\n\n- `CONTEXT.md`, ADR-0004, and ADR-0018 require the stable broker to own PTYs and streams while only the brain is replaceable; routine refresh/update may not terminate or suspend endpoints.\n- `docs/KNOWN-HAZARDS.md` §7.36 covers a suspended/black-holed brain subscriber freezing controller output and requires broker-side stall eviction/recovery; §7.38 requires bounded, cancelable, poison-on-failure physical broker writes. These guards address blocked physical subscriber connections, but do not prove successor dispatch classification/reclaim correctness.\n- `docs/DEBUG-ROLLOUT.md` requires local source-built, isolated-home test tooling and forbids production CLI/debug trust shortcuts. The proposed loop follows that rule: loopback, temp home, scoped PIDs, no shared release/update cache, and no live sessions.","files":[{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Closest real `spt daemon run` + real `spt daemon refresh` E2E. Proves broker/harness/perch survival and brain PID/generation change, but deliberately NET-LESS and has no rc/controller traffic."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Best N-session, real brain-subprocess replacement harness. `spawn_ticker_controller` holds raw spawn-time local controllers and counts output across supervisor restart/promotion; ideal base for a composed dispatcher recovery rig."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Contains `attach_survives_target_brain_restart_exactly_once`, real echo PTY and bidirectional dead-window markers, plus controller/viewer role tests. Successor manually calls `serve_attach`, bypassing dispatcher rediscovery."},{"path":"crates/spt-daemon/tests/dispatch.rs","description":"Production `run_dispatch_loop` integration rig with two loopback brokers. `dispatcher_serves_a_remote_drive_attach_undriven` and endpoint-addressed counterpart prove Request→Input→echo, but do not restart the dispatcher or test retained-ring floors/finished streams."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"Critical reconstruction seam: `run_dispatch_loop`, `worker`, `peek_first_line`, `classify_first_line`, `spawn_dispatcher`. A fresh per-process `claimed` set reclaims peer streams; selection currently does not exclude `finished`, and family recovery depends on retained opener bytes."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-owned NetStream data plane. `StreamLog` has a bounded 4,096-transport-chunk ordinary ring, evicts its front, supports one replaceable subscriber, and preserves stream info across brain subscriber disconnect. `NetStreamInfo.finished` is available to dispatcher selection."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` exact controller/viewer server path. Uses separate receive/controller and wire-forward Brain connections; subscribes the NetStream, replays Request, attaches by origin, journals input, and forwards sequenced PTY output."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"`run_brain` startup and supervisor lifecycle. Uses cursor-only `resume_session_cursors`, writes ready, then starts dispatcher/peer pump on the first net-enabled heartbeat via `spawn_net_consumers`."},{"path":"crates/spt-daemon/src/brain.rs","description":"`resume_session_cursors`/`resume_sessions`, attach APIs, NetStream APIs, and planned brain restart request. Documents why the supervised main brain must not subscribe PTY output on its heartbeat connection."},{"path":"crates/spt-daemon/src/broker.rs","description":"PTY owner and connection cleanup. `handle_conn` detaches dead session/NetStream subscribers but leaves sessions and stream rings alive; `dispatch_spawn`, `dispatch_subscribe`, and `resolve_subscribe` implement spawn-time controller and same-origin silent retake."},{"path":"crates/spt/src/rc.rs","description":"Operator-side production viewport pump. Local attach is a loopback NetStream through the same `serve_attach` dispatcher path as cross-node attach; detach finishes the viewport without killing the broker-owned PTY."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Closest existing real detached daemon + dummy harness + real rc subprocess rig. Covers abrupt rc/PTY failure and later attach, but no brain refresh."},{"path":"crates/spt/tests/brain_respawn_rename.rs","description":"Real daemon process and applied-binary rename/brain respawn path proof; has no hosted PTY/controller assertions."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Deterministic real PTY flood rig proving cursor-only daemon-brain resume avoids request/reply connection self-deadlock; adjacent subscriber hazard, not remote attach redispatch."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"Real broker/PTY controller black-hole and physical connection retirement evidence for KNOWN-HAZARDS §7.36/§7.38; no refresh or dispatcher reconstruction."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Windows deterministic black-holed-controller lifecycle harness; proves unrelated sessions continue and bad physical conn retires, but not brain refresh."},{"path":"docs/KNOWN-HAZARDS.md","description":"Historical controller/brain subscriber hazards, especially §7.36 broker-viewer-brain decoupling and §7.38 bounded/cancelable physical writes; useful to distinguish prior blocked-connection failures from reconstruction failures."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Requires isolated source-built debug validation rather than production update/debug surfaces; supports temp-home, loopback-only feedback loop."},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","description":"Original stable-broker/restartable-brain and no-endpoint-termination update contract."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Restored process boundary: broker owns PTYs/sockets/NetHost; brain-owned consumers are reconstructed after replacement."}],"architecture":"The broker is the durable-in-process data plane for a daemon lifetime: it owns PTY masters/children, output logs, input effects, NetHost connections, and NetStream rings. `spt rc` does not own the PTY; it owns an operator viewport carried by a broker-held stream. On the target node, a brain-process dispatcher classifies that peer stream and launches a `serve_attach` worker whose broker IPC subscriptions temporarily bind the stream to the PTY controller/viewer slot. A brain refresh kills these control-plane workers but must leave the broker data plane untouched. The successor main brain deliberately restores only session cursors on its non-draining heartbeat connection; independently, its newly spawned dispatcher must rediscover and re-serve every still-active peer stream. Existing tests prove each segment separately but not their composition. The uncovered join is successor stream claim/classification: finished streams are not filtered, and active stream family is re-derived from a bounded byte ring whose opener may no longer exist. The proposed N=3 isolated supervisor+dispatcher+echo-PTY test makes that join deterministic and distinguishes session survival, active controller reconstruction, and detach/reattach recovery."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5193,"output":5354,"cacheRead":121344,"cacheWrite":0,"totalTokens":131891,"reasoningTokens":1149,"cost":{"input":0.025965000000000002,"output":0.16062,"cacheRead":0.060672,"cacheWrite":0,"total":0.247257}},"stopReason":"toolUse","timestamp":1784196184831,"responseId":"resp_07547ad47f060e5d016a58ac59a93c819aa5d26b40ad29fd43","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_07547ad47f060e5d016a58ac5efa24819aa52f94fd4a2c9d3a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKxt6rIhwHdcj4s4gnwMftqMc45JX5rNShGp5kP4pe0J93PkWLDWqHgXqbd4v3IAVpT8_ZgQaKVQu7tZliS8Qi5M_IRC3HK_cG8S7XPITrk55PxZDAL2WYANbVHapFDiQNXQ8ni6VSedc8SXs5GASUBddAtac9B2QituGfFcEo5sw-tIjasYygUaBSO10PAcynxP7BfBeQOBVeR-xvOkKZuaYKbwqGfcxjGriL1H2pJ6BD4yVo8ClRjsV57qdoxFEfXBsM5O2ftmpGjukD7L1Nt1Ubp0Y0kdmSZ-MQWNPGmx7ThxdFBWjtq-oQTDSO3m7XaF-Qoy8pkgWAg6iVTn9MVfEJTBGJWXtdZ6Ts_Md1OLigNAMKRDveUx1nMB2sNEe2P_mVStlCfVM2mmoz56EU682O3CqdgdK-Ud1xxwinWm4xb4HUNIsjUck3y1RFOhB4MCcoepTdDC2XxgRl9j0X1JtqlL5Y9goM8W9GsBYCMz2fxWiufkd2QZjRdFoliYcbOJ861UJf52BToAirxMZ8B3AMlxDoFZqCSIOZek6nRsu0Yvrs46BMJz4K6kcwKYLeNoV7m5tDBz9lI0LaEuexpeMQ8SPPLERSPaZn44C2gr74Sdhd8NOeUu73s3mTGFL4xeYLN92osnr3J_HpGgTs8rQBQsUOxiQcAk9NI9uNzbpgeAqmW1peVFDmVhGRaS3H7qjk2B8cQWi9nIdAZcvqDVD7mSgADX6k4wFax22eBBKcwgbjsjg0KxtY5raqmMj36VOjN2Xegf_0gjreFhyV5GL1h1q8wYIRBHMFVJEke3CAptHXLPp9_Uhs8v4G57kEwzxb7WTt0YVd54XOoIejlvu8tRv_mwrmLNcq7nhHdvFS7oqyCYjSParS-Ku_pa9rKnFEFpHa1yDlbyYkicSUztLDshzbctUsOfb-yEuKwZg-aeZLTipEFWzykQoL9oYNnNsUA2wM3GNVVfxNrt8KWvdMcHvVEv38LCiAjLAxCVeHfUwWyQWhTm1GUOUXnALl1VmGmeNlU3g7xH08AlT09YfwBPhnFGIzlWiUapnlchoTO72V9ayHnAmhN62ATRxapHiBixLw2qcb2dXJUuq8uYC1P8RaMgN-k9CPRzLPWJasPgGBBgnRXZzmRFpYdYgrzxFFdp9GkBgYdslrwCbakXhnyia1URBNEu3-PAuPL5Zx_WQjPcvwKqXz6fm6MQJHRhjxW8RWH9h1Qr_d0rw43rCEiwzJp7i5GbdN2abHtVhvK0qKuKkIjufThVJZ8mfGK-cmUWaZ2UKy1GBLkxTHFMKPKfrbvTRXTSy5wjqSDfdWf72qGqUlRGRF-xnlWST1lJhB1HDTRBM3MGMZ5zyU43-Slk14e_4XoC_5janC2ZRwdc9JJDo8HH70LzQ8HYRpal8nScAIAwdlEFzKkIpRAa4BLt9rzfz9CXRuWG_YbswMMLJ8kA5J67mb0S-2uiIKnL3fQ7DBfJ_rMWTPxKKtz1qK77alDkqc0tgNWwlXtKw8xpkZ27c5yEE_Fa-y0izA6I3AS1IAZ2wf6WO_6lfSdbx9QZtjkvubDukYNyvKfYlBDtvkq2vLXUjP6HJDT_m4hfRZe125Qwi0J-CUmoj0_o50r2C3hYEhQhCvTSjrx4oAwEj8pVZjZnBYUzyMdNYyOl4a_X5Gi7f5WNa4kMxLsOj5WKxR8V7-Nlyn-IH894SIiLwsmVCHiPiYnOGQNLA2CCSWa0SFd6qTEZQEz-iQG5CiWio49rrnD5_qZGWJvwmLaEautxKWowOKmA2vbW813tR5ndHvParjKx1QolL39cIdispSOUM_-8ynCQY-BQgkESBryt4In13rOjNCS4BG3bkGdD43yDKbPyX7ara8VCdkNNxgzXxNIQrVYYziOoqd2k9-78rFJSUBV5Bj0QRRovEoWNA82NwSs3USzT4nwsNyYFk4DIuzho_d2tzRjZBojs5RIWg8_0frmc9VpYaKnnC__pgZuUUfgG617UZAZpHfyIDzErqd8qoxgDMG-Z4Tgezfjd1awXEs6f-H6nLD16L3LczgaBhMlmQ43b3Kv8NEbs4YPXz0aHkvkk63bgQUkXPL04xSCNPJnhj8Fx_9B9mJFJhIVLIHvFgdgNTiZfmecU76u_c-7LSOj-34Z7Bsjp4S-XwK8ZQDBedHoMRfHACVbYxXzkbZOrvT7gnnDRMpjsgPkVBjQcQu6EIPpYLbFYtDR53Oj70S_PgBDh29FyCQFh_U5IUihUOujpRG6KBhVOa0BGNkyvfIqhSKlIiWeC4g2s_MIFvQi7MUmrnJPHTcV_g23mbvozNJjuZibhPvjmcFQDoPazCLebpEjsAsEjMJaiNS4GxKHww4hjvXOCqCGeSXSM_N7Tr94gB_y34qTfKZmVyQIk76Z8k_zpEvbTY6Fugr-t5ZYGHTJxGqSa4h4AtGATkqjVRnloRJLA5oopGSZyeacZCt_bVJMqbIse1qrEOyaCGZn3RqShlPFpbrGjIROt9b2R8okoxuTZC0fa5JLzbc4bR2Y0lLhIaNoSsyDeja011tJih_QoqN4k3_HArrZ9M3FY_QfTGv0Qi7AjX2goGyoJa-bwB0CulqNXr2CsF03F25RXb6guYIQjtnOXMMX4hHV2crMpZI65fH3voYfBHDERUtFzmMXPC2nkpr_3MLkAinl5W45Lmhqc_hT1cmzYvoLj_WuFmt3fqVGCB2XKiSO_dKQMpJ1xqC41PUk7iUzxtSeOyNJVE_WGhviAjwifZkDzLFP7Vj0EAdPfdZezTpsieflj2SsLBxnh_0mDYqJZmPf3QanobI1mu0BxESnG05PjZA2_bp1M_si-0HuM6UV3eJzTsgZVJc1eqkQ78tJqwrRRM8hI9VbRGbHtG54dtLM2QDCdSxmP_VIap5F1l1nEg2ZnsSdhvXIBWbwmOjwxs9V1dBlHUE2g_71qhOXXivvqGrpqkunT_U0Y9EiVeFpTPGvaYR42sQo1_0Xp-xMoNHM0EDfqzIp2TsVv7dPH5Ja9xrDRqmV7c0eRsPtvX_J1O9tnPWOazgCmrYc9oDa20l0h7ei056rmPXXWnHHI8gFZqfGm82RBSNauvuHWy4lxwdas0o2ecPCtJLzmZxWmjik2O4Bus9rVylwOrcdVGS2gUjxi4yurPq6rEUy4bts4BQ6NPRV73KFduZUbVld4MA-6v6R7Cbhdes1FrvMxYCcYhfxb9MbEZWY3T0bGtS90YQCBKKPchQ8U_DFWFh3QN5wRIn98KDDTdaxSg5wuBVKz4ZbygNOUToPoQcntSdGVZOyyEIs3hletbRlTdtXDE7cwm57mcqDZhN5W8QmtbGLGSeHiem2T3C--oduPldZVUgkic_gbvELPlMIWU-dfpwOYAmM15f9dDIAQxJt3GaNTaE4RCzXyOv_c2anpV65-YUWGZOFDXwwWQNp8fAnY1EWFQdgsgxl328uwDXY5-0tVm2y9uGiOJHLgpNNZDqWsVTtopc_MuSl9kI8tsuFQ7o0Sx9err1_jwGDwvm5s3YwfuSng4awPVkdhEtOefCk7LGa_J6sTIFYLuN3sohNFT2B701zdd8cbHzYIol5Xb-2KJ7zJQv281W0Py9P5qzXCub1Nik2na4_qrjotxMiYwhsTt2m6Kpfb-jrE0yZni1JO3I9sBGwzOK--V8K6FYc2OhUL0fzta7NtwfXSKr9PkL9tc0IxcXtshwfCE0V_OB7zsZPQzQIKk9JgWjEmsbusAScnkdFoyOL-KC6OHQC_GhM-45EH_Z3qaYyLX2gb2u-x2cTfBd69SDKyclmSivzSCFzw3l7NM7A0iTzDuMH2MiIDqjj7HSfvxnFsrzsPlwEsT1xxGv7bPhUx26s50U2vudGKTYYORelCPP23S9lyZut0GyYvXdKCVaM39yqJYEMuhl4p7CrLC0ZHZZywzjbFWHpZ0j2ntr9ztfQ_ke-Q8dtdSvM0g3jl3slWzvQh4cehTm66vLbdSckx4fFrHRMA_wUgBpasXq135O3_woYEluNpQhMCS8GnABOhuwLkSBTPJMSqqFy8pFV0RJ_Nj4CQXMeYWdc8ybN_Y4BEGaqp-bUbi0UavB49pt6eJ3-_tRKwBnbCbqTfTbdUcyl4s9gOEEaT6F9-LwUCy3AuU3CCC6utjvEoiPXRI1uBUZ1gvY2sQunJogw1wmpLFkfS6Dh1IgxSTFiG6iOn0l5KYIWOAnX1dOaqu3GKzDsqM6zD4FZUSmQT-AQR9EoXUB02tgRpIVo2bTeEsUu7L7PBwXX-K23PZ4LGkxIAc0YtMcVg_CSkCE_HBOw-zw5HL5Qo1RABtnZDKkjqNUYGyQw-2VdD1WrUgVrvU9XZcv1c52u1Zxe-uKSVsOqbNu8zg2nqv9bKKIyA4JFyOWq3h0J_yMl3ISbIsIap1jj4D_EBR4PhJQl2m4BKCj6KbrCl4EPby6l0FAdD8CnZx2I5tzXZjnhvzSE7_fW3gpnMC1aOpvUnafuyDnEQqarNApVG_v4yPRBB3p2PQqBsLGLlAx78F4bbf-mPHamLh16glGpanj7_vEQ1FBV8LMmXBaMFJHiXdkqT3XgT7IK-n9U=","summary":[{"type":"summary_text","text":"**Summarizing test findings and architecture details**"},{"type":"summary_text","text":"**Detailing causal inference and feedback loop architecture**"},{"type":"summary_text","text":"**Confirming finished stream redispatch race condition**"}]},{"id":"rs_07547ad47f060e5d016a58ac6d31cc819ab0b69e931cd0e4e5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKx-bUfZp6ZwQA6lVdNB6_SfJsE6wWhQAI10s5kS-vy53r8Qsj1b59noyVuwAMB3a7pmy2GqnlPDbe9PX-SBNsNwAdC6p-S-2YtQPqDc7C1dD724D-EIyH0MfuXKPgtnW3pkzMPYLbZse0eg7ukhKph_IWKBAJxAazGnUdmAaea3fnYUWKHxkYEdhPyzhG7BKxmlUfNOu-oWT68tFeD1oBciY_W8p1OXrkr22uJ4qwkIfmroa92GkiwL9Uz37DC-j2K6gSheNl9_z4t9GH9XzKiJSnFhfQluPAJIuGuFm18mu-0gCLIb2IKDyVtFPcdXa6FEmHnAtoBPkryFlQA6D8qJcJeERjSPNuRIVe62EbxWEE9CyVnh-XZmo6kZMjwfM-WZ7vYdpWOQt87yOeOJZue7VCvQ7nPP8hj6sytj7FbtgNhkUSLwm4mhWqnH631CDX7o2qAfvYX6CBiBE0eo1WEek-wsfzNfqnwK0gfqU9pCeFG6dKHkw3VEJt-zP76zpVPxtCJPeiRzU7966rsmGIL-TKMLjrPeD74uR_4slXaE5ZytR-GToofFvFrw1HVXEflWPzXdk3GQk3GOR2L6m_mf-3H-0tb9MhULoXxIV3lpMgnB57nKn367XJ35G_qVH1XUy598ZsoSkA4npe2cQUa6m9tj5QqvYcQuhHLYAfyCs_SSKnzis6f4GO9pb4jZevQ_kABiYjHbYjo_k2Eq75l2HzFJZSaKdnQgxmpkvi8Z2zPv-wYzXzLxGTVjdl8pA2vOZNPgVUwq2CFnGuciVFaC9nre7GU9nEDoOJ6D1aAmfDdU5giIj41KxEWgIAa-YIJQbA0f5p2LKD2lDy89-yn0NRPzzw7MlrZMtreRwaczS-sjL9Ji0aU_uFLmqfFla6IVQo2yBY9UWkH_lhkS-00MnC3OOldYHoDWdsO5FaaIroO5B6ks_e-hZ1owZEILYLZdhzhqkA53Uy6Shq0oy6mL1FSqNKx5djwbuh1FhNqaezFo6UoAfOsB5j-Sts_4XFEGHnMMjamcOaIVABZl7MJ3OqDAmm5df1MsI4IsT_jy6xu-8eWyJx-QHqap7dGtREdLWUvplMe2XCuC1-CC-P-Cei4tAjBEH7AH8T_oNxfNuhZH2xzqJEUty8LJfEDPbAtSEe7cNr6R9HAjVX1O1uz5sCOZy6rzwWpuDnZ3ZYl4rKj54bGIVFOfeKjA-_8tyoWUepTWebEpAP1_j3F3iaEQPKeDixpDn8ZqGrKVazVuCBj5hU9l9Yne3yZC8XmRnNE9b18yFReUl1h2RafqgomPXQP4vrjdcSy8M21xkOPUH-L7Z7GQdi7xKGmUz9GGqHO6fAP84qGNQ5URst8IrHTfPdm5OudXUJtbDMq1wnbicoDUJYUgevh4uBIYHrT1fQsUwmq9xi6X3J4Z1R4GzyNZ3MrF5A9I1NEXRP33odh8xULkBmjOjuFlbDSxEc2Tqs6EqUASxJ8SN30TTStDTxsm_4NdzxMArrdtVQnBPmnh9yU2lRVldDpyOPwFNdQqbu2b88wHlseyTwrAgotbQTl8CZASQi0dSZRmGEPuefnYYSpdgyKkQessE8Wdwy3c1VPJHBLz8BYYQYrN9FzBHbywVy4UsLgJL7MP4PoqXmDYIQYqpUf0zE4esPaR7Pf8hmRZ7GUcw0vJPXuqDJ-0gsVrWXz2a5hA4B_XSWz9pEuHjdQx0QUhWP4djTgXSn_x_uUGXg8tWzoqBCqOvTfBbIcjVu2UYU9qbM7dYfQThHMJ5Gr5DlsMAyd8hbjbjsUqqTX_SlocXpWyhoubWdjr9gre1icUvTaYsuWurpAEYUvzYG3J5kV7yJeVdq3pPHGCzuhGJA0iQnAwIDe_efrC9gPIWAHvUHcNJbyP9G_H0UbXjeEDvMCobZdmK8CLMJIVB3fQa3IboRP8FPj8BacT22tpBn3sCUveqC7sO3zC8ogK_IdYe7lsRuAoWr72J26HYcy-r18cl3jUfDIe_lG93iae7Mhvx2oULSgks-G8K8DVJ8KD6MqnRV7kZvwxN8yntq8LRv3wFaBZIMNiATxtYoExwfU7IjGToKg_VfgyZPZfr5yRW7j5dJMXR9mIk9hInJ3-YwDO2t5mMpunLmR2mysJjE-BTwXla9IuZaV0jKbn4v8kypA4zxBtkr5yQ-lKiAT00csbvc8Vo0-FI8PDyjkbFdUnRfJ3Zi5g93nEl7irt3a-QY1Z4HbZPyS5mk-LcA4lVTqTtJyPeED05q8OPxomT9MPD_fVfg2Mn-74WEp2cUAHtWASE2OI8VvAdYMoTtFcjCgwMcyadBtgmqv1AGi1uzjZjR7sFwefN2bwUebPi4PrLEDMIG92-K3WWiiFxsTcxu1T33X6fqrKcMPj4HRELyx7Hyh_YWDL13QN6dnb2X3lKs6Ma1dKolE6ZE43XEiJy__Pwx7KRJyucqMoUVNtnXeE45OOc_DJ4zqZTUPlt2ttCd1s-H7uf9yxv75O3l2xlL1DY2IOIV6CcSZwDqfFJjAYoAweUFByGW0TCzjpNvP-c8UBE7w7G3B9VbC8i_8UcBkzxBkCzU7eLEqAxCXPBuLskFyYmJkutSHv4X50l4W1_G8lmDAhBuNx2OFLv-oqLujxS8G3I9XRjWvCItBwXHZSiBHduxtDyW_4oLm9S4bCxdt8WEt3U1NJrx_YTowKvWD6BqlbzizDfyAqAwDJkhu2zEC2RIFGM2YMqhmpRYhDWFQaUO_VoBpCjZQhpkzShDCBAeePNjohSX9QztO99xb2Ed7R2BjZCY4fh-i-wAPVMr5AtJgmm0LegPpthwM0Qtyx5WfPsYRGLF0UoCa0F0kX_z0NI0ChW9-lWNAqYJCrS7cfYJY1MGAX3l5cVYUFXHv87S2iZw1DZhg26enMk5t5KI7im5Wqc445l8d4bG9zjj9KesdL7RrYQ98ZJFqyVEhPwlSPT75DQo3YsZ19svvjdUyl9SGD9B1-Miu5iqp-XXGgLyFelLBCevheC2LqpNBC12JBrTzVdua5JiJ7AHOmsZgYe4nLgVlPPGAR_JYuseKy4iDBNJVsNYnDSnGJ8FwfF-SSErcjdaU1T34yM9egtM8Azb1FiKkJ8SNaljzL2H5y5oCUsynHGXfFPol_RQ8rKvGDj8vt1XYCOptXh58WOYr_S9oMy0p3vbloSSZxYdMNc5ezeO6y4tIN3XzDPvdt3OAUJaA9q7Hj1GgAM-YovK8-DSSYS95D0YevX2a1JbeEEkTVYJwq_mOI2uuSBkwBzfcDhIxIiWDw96n2w600Hl3opSkPQhvh67DFl1eGPmuJ-Q9rLPWi0LDoISAkh5QawOeUEGpIFm3AioeMuo9dObBMAN-pJReV2FYctVImObA0vyLjuHUjNP8tsdP83it6Hj-dMcK9v_mLKwvhKD8abvgxbLcJJCI6br6knPJXg8uPcvh88IqZKcVY-dEBTg16GtwvE7Rc7foN__2W3aySe3S1CORcQJyPwikbWYUCGJvwTLFnfttt6aDPJ-155_LofzETXtTUYbHEASUFpTyQ_DJNbmET4HL5GrumUfjN_eHcCAAjKSgeFehx8lVv7ILTjYVQ4e_-aAD9vgC5H7ZHJ-J50ptUgiivDPsJRCKPp0XRoziNVOS9qwYGdW3YIxs37omeXZ57ja59sEFhBAu9alR2oGHarvi-IWGdj9wwUygwokXTdLIrpKjmxbO2w4k7kFDPjbfcTY5O4rW7TTa230a4fwc5Y4DaBKsRDeWxNre9U9BtUlG6TNdPc99JpMmNhiK3ZzPkOTzbznLIdRyW-ASgYrTqMbqYd2lz65WE_Rmi5EXnaO8WdxY6TniqayYOsFDl9BQTa6TfPmgfVroWkTeTHnfqTuY80TPi29yXlplmBbpE9FCV2EzJxI0GGfme3Bj7sABrm4mk3tmjuDKVnQLVS_VHeVp1n5EZsgoVSu2nFNmK5P0PNJxoawoIGRYMEqX3Ddu_0gWHWcTS_SNNk-RIJzLrSSFmXmG6he-ipCUGHCCw2GWU-PSEjZLn6Ra0av3pwMAtk07T9gTX-OeRYPNt-PVNUQxl6aQFCSF5a5kipAWyYnoUyLdU6ggRtx8jTQHZWb5z64xyiKKvwgjY81_OFnm_MaufhtMjcf6a4LAI787IV_j_4KJPa1boV6fbQFWVlQMrsjQKvg27q6xusUeuzL-8OIKkVKHkUUKtj0gPcAAG3lQo8vJ-1R4ubRb7t5QZty_vxPohVhbqE2grFHWJpgkJ_Fm4i9WOrzjFjIbP8XNl52mg2vtP06wiiEXb1l7YwzmLbSWR9gEAtLeuGGiANyvqHmI-a3FHqz3zn5s5TxnAi6yj113UkM3GhvCztquvRM14hYdcB6pzUCZAsRYN7lQQIF975QgZ6Uw=","summary":[{"type":"summary_text","text":"**Planning tests for stale stream races**"},{"type":"summary_text","text":"**Designing two-phase stale stream tests**"},{"type":"summary_text","text":"**Confirming no tests executed**"}]},{"id":"rs_07547ad47f060e5d016a58ac7e271c819a8528087014d9844b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKx-JxdrutocO9edZUqcpvg_Dtnmye4CL14OZsgT8vSPnZ_-sbFVS4_NrZVJBY1-RU5YTcMWC0pSyBlStixAuMBFv-1X-95mqyD-M3OfS2ILGXfB-SJHxZN3eH9bTnoAg6s11aDl9u87cZS15yGq-wTbXdJI-SIjmYLEwZc13PGwEfW9CKGuzXDJR0pFNlcYPRI4dBQBjKkEOqrtija3rostHshkgQx_788F922wfKCblSBjVJ53z18pTpXvnGzpz63Var3xVTD4ae_diNuyUhONPX1bt4-FbNQyaypPVGQKl2nB5i02cwzWIJYuksV2wMIrQ1Z0yR33r_qzOg0x062hOwE_qLId_Rm8riOu-wlYFEuq8PIEUsyiSy5THS7U4voqEr0CqcdKUpTn4kXhdPEUrHtsSBALzF0eKX5hUOptFpzq1n_KS8yu6eGgMWh43lXx1tsyElTP0g90PsMJDB19n6y3xJ_PW-sFfpJfc9cO6Iqhpi5sf4xhD6nU0vL5KjSwfPgwjG3oNmqND22ntAvlzzC4mcnudb2KRxamiTlMeSJWcl5Gfxy3ZQHieotiDM_q4nxALug4wQMkT2yxKcEtbqusRNFWITcA4u06yGmMEd4lb5lUKkpLGdePns4oMddVlXEPz9UtWgYRDJ7vQYrCeRrtAvXmp8taGfUkFZ3eHn0-Ck-VQ2ERW4Yyr7Uw90lwUycfv3ynecqPgJieTPsghUSs96z3Vv3aoaNEWIw0hZhnV06kfkalTVdNCrK1lacXMxWWNt-a9KV2YVztUVkjlogDc6K9oGdWeCalO0ywZej8aaMqqNc02uhpf3QmWoS0ukQ70vX3vxnJ5MxwHwJ9lgJM5HPA01HnAyCxLIbPZuWhJzNc8q6Wa0HRPkkmyUdDzR1kgeFSrgtSVKwu_jTpcEwcmVAaTRVMyf-ITHdtusr5qVJ2kuN7UEvTna-wzRr3GRlK2KYPZqYTQYytGVeCKTTSARsea4nU1kR6coXSx7cRngEi1cHhkl9Eqypebb4mTjevlgB9sYLKMj1Ln8wlnIKEMu8gbRgiU1Wsglo_fpLsFzqUqeHMqPgjVNOSwfNMecGW3f0-MVzM-pNgKgN3n2VQf86r8XX6fUb23sDIJcJmnymb9AiiLfkfWo9BgAv_BGrUOJ36KuDNGfKqoNDtO0_3dshvmMvXwk5djRUAc4mXtpSRpApYgsnOePTqo0Q52sNTbGfKHeoLWJdLDJ1sTbzosx3nfo0jJDljh430btUnpREvkXhRojdcMRpxbVKYUW8HcMPxzoJHqfaV4-cuCD-bf-PYhpqVZE5lgsKuRVngbS3uU_t8b_w61dchKgk9-xJz7YYzhU54TT9eVxkTuT6pJRqL41gu1-oVTwDGzsACc3iht0rfyKF2-F1yEdZCjCUDwWJPO5pNvlfpLmUGbA5hBKnMV5R_OBafJ9qFX8LGfvaS19-dzxmFLeJbi0kNf4yzO8bQAfJYzFQOIwwOKPItuBczHv-5eGcW04hQfFMpw2LbwV4S0hpsfrY2cJms","summary":[{"type":"summary_text","text":"**Reviewing nextest test commands**"}]},{"id":"fc_07547ad47f060e5d016a58ac7e8bd8819abb76990555258fbf","type":"function_call","status":"completed","arguments":"{\"type\":null,\"result\":{\"data\":{\"summary\":\"Investigation complete, read-only. No refresh/update was invoked, no live endpoint was touched, and no tests/builds were run, per the assignment constraints.\\n\\nCLOSEST EXISTING HARNESSES\\n\\n1. `crates/spt/tests/daemon_refresh_e2e.rs::daemon_refresh_cycles_brain_while_hosted_endpoint_survives` is the closest full production-process refresh harness. It creates a temporary `SPT_HOME`, installs a private mock adapter, launches the real `spt daemon run` broker/supervisor process, starts a real broker-hosted dummy-harness PTY, invokes the real `spt daemon refresh` CLI, and proves brain PID/generation changed while broker PID, harness PID, and ONLINE status survived. Command: `cargo nextest run -p spt --test daemon_refresh_e2e`. Critical limitation: it deliberately writes an invalid `identity/node.key` to make the rig NET-LESS, starts only one endpoint, never starts `spt rc` or any attach stream, never injects PTY input, and never reads post-refresh PTY output. It therefore cannot detect loss/replay of dispatcher-owned controller streams.\\n\\n2. `crates/spt/tests/resume_no_control_steal_e2e.rs::brain_respawn_keeps_every_session_controller_and_still_promotes` has the best existing N-controller brain-replacement rig. It runs a real `spt daemon brain` child under `supervise_brain`, an in-process real broker/NetHost, and N real ticking PTYs, with one raw local controller connection per PTY. It proves every held controller continues receiving output across a real brain-process replacement. Command: `cargo nextest run -p spt --test resume_no_control_steal_e2e`. Limitation: these are broker `KIND_SPAWN` auto-controller connections, not remote/local `spt rc` NetStreams; it asserts output only, not operator-input→PTY→output round trips; it does not run the `daemon refresh` CLI.\\n\\n3. `crates/spt-daemon/tests/attach.rs::attach_survives_target_brain_restart_exactly_once` is the strongest existing bidirectional attach-restart proof. Two real loopback NetHosts/brokers host a real echo PTY; an operator sends `MARKER_ONE`, the serving brain dies, `MARKER_TWO` is sent in the dead window, and a successor re-serves the same broker-held stream. It asserts both markers on the rendered viewport and source PTY screen. Focused command: `cargo nextest run -p spt-daemon --test attach -E 'test(attach_survives_target_brain_restart_exactly_once)'`. Critical limitation: the test already knows the stream family/id and manually calls `serve_attach` on the successor. It bypasses production `run_dispatch_loop` rediscovery and `peek_first_line` classification, which is exactly where refresh recovery can fail.\\n\\n4. `crates/spt-daemon/tests/dispatch.rs::dispatcher_serves_a_remote_drive_attach_undriven` and `dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint` exercise the production dispatcher with real brokers, NetHosts, echo PTYs, attach requests, controller input, and echoed output. Focused commands: `cargo nextest run -p spt-daemon --test dispatch -E 'test(dispatcher_serves_a_remote_drive_attach_undriven)'` and `cargo nextest run -p spt-daemon --test dispatch -E 'test(dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint)'`. Limitation: the dispatcher is started once; neither test kills/replaces it, cycles a brain, fills/evicts a stream ring, or leaves stale finished attach streams for a successor dispatcher.\\n\\n5. Adjacent evidence: `crates/spt/tests/attach_wedge_e2e.rs` is the closest real detached-daemon + real `spt rc` process rig, but tests abrupt rc/PTY death and subsequent new attach, not refresh; `crates/spt/tests/brain_respawn_rename.rs` tests a real daemon/binary rename/brain respawn but has no PTY/controller; `crates/spt/tests/brain_survive.rs` tests process-level survival across swapped brain bytes but not production attach redispatch; `crates/spt-daemon/tests/brain_resume_conn_deadlock.rs`, `brain_decouple.rs`, and `conn_blackhole_lifecycle.rs` cover non-draining subscriber, connection retirement, and unrelated-session liveness but not the remote attach dispatcher lifecycle.\\n\\nCONFIRMED CALL CHAIN AND OWNERSHIP\\n\\n- PTY data plane: `Broker` owns each `PtySession`, input writer, output ring, controller/viewer table, child PID, and effect journal. `broker.rs::handle_conn` explicitly detaches a dead subscriber but leaves sessions running; NetHost likewise detaches a dead stream subscriber while preserving streams/rings.\\n- Controller/viewer connection: `serve_attach` owns the serving side of an rc viewport. Its primary `Brain` connection subscribes both to the peer-initiated NetStream and to the PTY as controller/viewer; a second `wire` Brain connection forwards session output back onto the NetStream to avoid same-connection replay deadlock. Controller input is decoded from `AttachRecord::Input` and delivered with `send_effect_no_ack`; output is encoded as `AttachRecord::Output`.\\n- Daemon brain refresh: `cmd_daemon_refresh` calls `refresh_brain`; the supervisor cycles only the brain child. `run_brain` invokes cursor-only `Brain::resume_session_cursors`, intentionally making no PTY subscriptions because the main heartbeat connection is non-draining. Thus `BRAIN_RESUMED` logs session cursors, not rc controller streams. This is correct and orthogonal to rc restoration.\\n- NetStream/rc restoration: after `run_brain` reaches its heartbeat, `spawn_net_consumers` starts a fresh dispatcher. `run_dispatch_loop` calls `Brain::net_streams`, selects peer-initiated streams, and spawns `worker`; `worker` calls `peek_first_line(stream_id, from_seq=0)`, classifies the protocol, then creates a fresh Brain and calls `serve_attach`. `serve_attach` replays the Request, resolves endpoint→session, and `attach_as(..., Some(origin_node))` silently retakes the same-origin controller. Therefore the architecture does contain an automatic rc re-serve path; saying the successor only restores `BRAIN_RESUMED` cursors omits this separate dispatcher reconstruction path.\\n- Broker lifecycle: a brain-only refresh must not change broker PID, PTY PID, NetHost connection, or NetStream table. A broker restart is a separate lifecycle and severs these ephemeral controller transports; it must not be conflated with brain refresh.\\n\\nCAUSAL CANDIDATES\\n\\nA. Stale finished-stream redispatch/takeover — primary candidate, `[INFERENCE]`. `NetStreamInfo` exposes `finished`, but `run_dispatch_loop` selects on only `!initiated_locally`, `next_seq > 0`, and fresh `claimed` membership; it does not exclude finished streams. Every brain generation starts with an empty `claimed` set. Consequently a successor can redispatch historic, already-finished attach streams, replay their old Request records, and transiently/silently retake a same-origin controller while active streams are also being reconstructed. Multiple historical streams can race in HashMap iteration/worker scheduling. This is a direct candidate for partial session recovery and controller churn.\\n\\nB. Evicted opener makes redispatch one-shot fail — structurally confirmed, causal application `[INFERENCE]`. `NetHost::StreamLog` is a bounded ordinary ring of 4,096 transport-read chunks and evicts its oldest chunks. `run_dispatch_loop` nevertheless reconstructs protocol family exclusively by subscribing from logical zero and parsing the first retained NDJSON line. If the original attach `Request` has rolled out—or the retained floor begins in the middle of an NDJSON record—the first parsable retained record is typically `Input`/`Resize` or invalid JSON. `classify_first_line` deliberately returns `Unknown` for mid-protocol records. The worker exits, but the dispatcher has already inserted the stream ID into `claimed`, so that generation never retries it. Detach+reattach creates a new stream whose Request is again at its retained head, explaining immediate recovery.\\n\\nC. Partial 4/5 observation: `[INFERENCE]` both candidates naturally produce a non-uniform blast radius. Finished-stream history differs per endpoint, and opener retention depends on the amount/coalescing of operator→target traffic. Four older/high-input streams can have lost their Request or have more stale finished predecessors while one newer/quieter stream retains a classifiable opener and re-serves normally. This is more specific than an undifferentiated PTY freeze: PTY children and broker rings can remain alive while only selected controller forwarding workers fail to reconstruct.\\n\\nSMALLEST SAFE DETERMINISTIC FEEDBACK LOOP\\n\\nCompose the `resume_no_control_steal_e2e` real-brain supervisor with the `dispatch.rs` real remote-attach echo flow in a new isolated integration test (suggested name `refresh_rc_stream_recovery_e2e`; avoid `update`/`setup`/`install` in the test-binary name because Windows installer heuristics can cause UAC error 740).\\n\\nUse a temporary process-global `SPT_HOME`; one in-process target `Broker::bind_in_with_net` with loopback-only `NetHost`; one in-process operator broker; and the real `CARGO_BIN_EXE_spt` brain subprocess under `supervise_brain`. No discovery, relay, production socket, production perch, or live user process is involved. Wire the broker's `BrainRestart` signal exactly as the supervisor tests do, then invoke the private-home `spt daemon refresh` CLI if exact verb coverage is desired; alternatively call `BrainRestart::request` for the narrower brain-replacement loop.\\n\\nUse N=3 because it is the minimum that can express “most but not all”: spawn three labeled real echo PTYs (`cat` on Unix, `findstr .` on Windows); open three endpoint-addressed Control attach streams; send unique PRE markers and require all three to echo. Preserve the Request in stream 3. For streams 1 and 2, deterministically shape the broker-held peer ring into the natural post-rollover state by calling the test-held target `NetHost::drain_stream(peer_stream_id)`, assert the drained bytes contained the attach Request, then send/observe another input so the retained head is a mid-protocol Input record. This avoids 4,096 timing-sensitive writes or hundreds of MiB of artificial traffic while representing the same observable broker state as bounded-ring rollover.\\n\\nCycle the real brain and wait for a changed PID/generation plus `BRAIN_NET_CONSUMERS_UP`/a bounded dispatcher-ready condition—not merely `brain.ready`, because current `run_brain` writes ready before its first 500 ms heartbeat starts net consumers. Then send unique POST markers down all three existing controller streams and collect results with bounded `read_event_until` loops. Required pass contract after a fix: all 3 existing streams return their own POST marker exactly once; all PTY PIDs and the broker remain unchanged. Current discriminating failure: stream 3 (retained opener) recovers while streams 1 and 2 time out because successor classification sees Input rather than Request. After collecting that failure state, detach and create fresh attach streams for 1 and 2 and prove RECOVER markers echo; this verifies the reported detach+reattach recovery without masking the original failed assertion.\\n\\nAdd a separate deterministic selection test for candidate A: construct active and finished peer-initiated attach streams and assert successor claim selection excludes `finished=true` rows before any worker can replay an old Request. Do not rely on concurrent worker ordering to prove this invariant.\\n\\nThe single end-to-end pass/fail signal should be: `pre_echoes == 3 && broker_pid_same && pty_pids_same && brain_pid_changed && existing_post_echoes == 3`. Diagnostic output should include, per stream: stream ID, `finished`, `next_seq`, whether opener was retained/drained, pre/post echo, and fresh-reattach recovery. This separates PTY survival from controller-stream restoration and makes a failed run actionable.\\n\\nHISTORICAL RULES\\n\\n- `CONTEXT.md`, ADR-0004, and ADR-0018 require the stable broker to own PTYs and streams while only the brain is replaceable; routine refresh/update may not terminate or suspend endpoints.\\n- `docs/KNOWN-HAZARDS.md` §7.36 covers a suspended/black-holed brain subscriber freezing controller output and requires broker-side stall eviction/recovery; §7.38 requires bounded, cancelable, poison-on-failure physical broker writes. These guards address blocked physical subscriber connections, but do not prove successor dispatch classification/reclaim correctness.\\n- `docs/DEBUG-ROLLOUT.md` requires local source-built, isolated-home test tooling and forbids production CLI/debug trust shortcuts. The proposed loop follows that rule: loopback, temp home, scoped PIDs, no shared release/update cache, and no live sessions.\",\"files\":[{\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"description\":\"Closest real `spt daemon run` + real `spt daemon refresh` E2E. Proves broker/harness/perch survival and brain PID/generation change, but deliberately NET-LESS and has no rc/controller traffic.\"},{\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"description\":\"Best N-session, real brain-subprocess replacement harness. `spawn_ticker_controller` holds raw spawn-time local controllers and counts output across supervisor restart/promotion; ideal base for a composed dispatcher recovery rig.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"Contains `attach_survives_target_brain_restart_exactly_once`, real echo PTY and bidirectional dead-window markers, plus controller/viewer role tests. Successor manually calls `serve_attach`, bypassing dispatcher rediscovery.\"},{\"path\":\"crates/spt-daemon/tests/dispatch.rs\",\"description\":\"Production `run_dispatch_loop` integration rig with two loopback brokers. `dispatcher_serves_a_remote_drive_attach_undriven` and endpoint-addressed counterpart prove Request→Input→echo, but do not restart the dispatcher or test retained-ring floors/finished streams.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"Critical reconstruction seam: `run_dispatch_loop`, `worker`, `peek_first_line`, `classify_first_line`, `spawn_dispatcher`. A fresh per-process `claimed` set reclaims peer streams; selection currently does not exclude `finished`, and family recovery depends on retained opener bytes.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Broker-owned NetStream data plane. `StreamLog` has a bounded 4,096-transport-chunk ordinary ring, evicts its front, supports one replaceable subscriber, and preserves stream info across brain subscriber disconnect. `NetStreamInfo.finished` is available to dispatcher selection.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"`serve_attach` exact controller/viewer server path. Uses separate receive/controller and wire-forward Brain connections; subscribes the NetStream, replays Request, attaches by origin, journals input, and forwards sequenced PTY output.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"`run_brain` startup and supervisor lifecycle. Uses cursor-only `resume_session_cursors`, writes ready, then starts dispatcher/peer pump on the first net-enabled heartbeat via `spawn_net_consumers`.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"`resume_session_cursors`/`resume_sessions`, attach APIs, NetStream APIs, and planned brain restart request. Documents why the supervised main brain must not subscribe PTY output on its heartbeat connection.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"PTY owner and connection cleanup. `handle_conn` detaches dead session/NetStream subscribers but leaves sessions and stream rings alive; `dispatch_spawn`, `dispatch_subscribe`, and `resolve_subscribe` implement spawn-time controller and same-origin silent retake.\"},{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"Operator-side production viewport pump. Local attach is a loopback NetStream through the same `serve_attach` dispatcher path as cross-node attach; detach finishes the viewport without killing the broker-owned PTY.\"},{\"path\":\"crates/spt/tests/attach_wedge_e2e.rs\",\"description\":\"Closest existing real detached daemon + dummy harness + real rc subprocess rig. Covers abrupt rc/PTY failure and later attach, but no brain refresh.\"},{\"path\":\"crates/spt/tests/brain_respawn_rename.rs\",\"description\":\"Real daemon process and applied-binary rename/brain respawn path proof; has no hosted PTY/controller assertions.\"},{\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"description\":\"Deterministic real PTY flood rig proving cursor-only daemon-brain resume avoids request/reply connection self-deadlock; adjacent subscriber hazard, not remote attach redispatch.\"},{\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"description\":\"Real broker/PTY controller black-hole and physical connection retirement evidence for KNOWN-HAZARDS §7.36/§7.38; no refresh or dispatcher reconstruction.\"},{\"path\":\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\",\"description\":\"Windows deterministic black-holed-controller lifecycle harness; proves unrelated sessions continue and bad physical conn retires, but not brain refresh.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Historical controller/brain subscriber hazards, especially §7.36 broker-viewer-brain decoupling and §7.38 bounded/cancelable physical writes; useful to distinguish prior blocked-connection failures from reconstruction failures.\"},{\"path\":\"docs/DEBUG-ROLLOUT.md\",\"description\":\"Requires isolated source-built debug validation rather than production update/debug surfaces; supports temp-home, loopback-only feedback loop.\"},{\"path\":\"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md\",\"description\":\"Original stable-broker/restartable-brain and no-endpoint-termination update contract.\"},{\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"description\":\"Restored process boundary: broker owns PTYs/sockets/NetHost; brain-owned consumers are reconstructed after replacement.\"}],\"architecture\":\"The broker is the durable-in-process data plane for a daemon lifetime: it owns PTY masters/children, output logs, input effects, NetHost connections, and NetStream rings. `spt rc` does not own the PTY; it owns an operator viewport carried by a broker-held stream. On the target node, a brain-process dispatcher classifies that peer stream and launches a `serve_attach` worker whose broker IPC subscriptions temporarily bind the stream to the PTY controller/viewer slot. A brain refresh kills these control-plane workers but must leave the broker data plane untouched. The successor main brain deliberately restores only session cursors on its non-draining heartbeat connection; independently, its newly spawned dispatcher must rediscover and re-serve every still-active peer stream. Existing tests prove each segment separately but not their composition. The uncovered join is successor stream claim/classification: finished streams are not filtered, and active stream family is re-derived from a bounded byte ring whose opener may no longer exist. The proposed N=3 isolated supervisor+dispatcher+echo-PTY test makes that join deterministic and distinguishes session survival, active controller reconstruction, and detach/reattach recovery.\"}}}","call_id":"call_iJY9LfYipMD9Hdk0bcwfQVKX","name":"yield"}]},"duration":129037.34779999964,"ttft":6124.5859000002965,"contextSnapshot":{"promptTokens":126537,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_iJY9LfYipMD9Hdk0bcwfQVKX|fc_07547ad47f060e5d016a58ac7e8bd8819abb76990555258fbf","toolName":"yield","startedAt":"2026-07-16T10:05:13.871Z"},"id":"65fab19b","parentId":"760dc594","timestamp":"2026-07-16T10:05:13.871Z"}
{"type":"message","id":"1b452802","parentId":"65fab19b","timestamp":"2026-07-16T10:05:13.871Z","message":{"role":"toolResult","toolCallId":"call_iJY9LfYipMD9Hdk0bcwfQVKX|fc_07547ad47f060e5d016a58ac7e8bd8819abb76990555258fbf","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Investigation complete, read-only. No refresh/update was invoked, no live endpoint was touched, and no tests/builds were run, per the assignment constraints.\n\nCLOSEST EXISTING HARNESSES\n\n1. `crates/spt/tests/daemon_refresh_e2e.rs::daemon_refresh_cycles_brain_while_hosted_endpoint_survives` is the closest full production-process refresh harness. It creates a temporary `SPT_HOME`, installs a private mock adapter, launches the real `spt daemon run` broker/supervisor process, starts a real broker-hosted dummy-harness PTY, invokes the real `spt daemon refresh` CLI, and proves brain PID/generation changed while broker PID, harness PID, and ONLINE status survived. Command: `cargo nextest run -p spt --test daemon_refresh_e2e`. Critical limitation: it deliberately writes an invalid `identity/node.key` to make the rig NET-LESS, starts only one endpoint, never starts `spt rc` or any attach stream, never injects PTY input, and never reads post-refresh PTY output. It therefore cannot detect loss/replay of dispatcher-owned controller streams.\n\n2. `crates/spt/tests/resume_no_control_steal_e2e.rs::brain_respawn_keeps_every_session_controller_and_still_promotes` has the best existing N-controller brain-replacement rig. It runs a real `spt daemon brain` child under `supervise_brain`, an in-process real broker/NetHost, and N real ticking PTYs, with one raw local controller connection per PTY. It proves every held controller continues receiving output across a real brain-process replacement. Command: `cargo nextest run -p spt --test resume_no_control_steal_e2e`. Limitation: these are broker `KIND_SPAWN` auto-controller connections, not remote/local `spt rc` NetStreams; it asserts output only, not operator-input→PTY→output round trips; it does not run the `daemon refresh` CLI.\n\n3. `crates/spt-daemon/tests/attach.rs::attach_survives_target_brain_restart_exactly_once` is the strongest existing bidirectional attach-restart proof. Two real loopback NetHosts/brokers host a real echo PTY; an operator sends `MARKER_ONE`, the serving brain dies, `MARKER_TWO` is sent in the dead window, and a successor re-serves the same broker-held stream. It asserts both markers on the rendered viewport and source PTY screen. Focused command: `cargo nextest run -p spt-daemon --test attach -E 'test(attach_survives_target_brain_restart_exactly_once)'`. Critical limitation: the test already knows the stream family/id and manually calls `serve_attach` on the successor. It bypasses production `run_dispatch_loop` rediscovery and `peek_first_line` classification, which is exactly where refresh recovery can fail.\n\n4. `crates/spt-daemon/tests/dispatch.rs::dispatcher_serves_a_remote_drive_attach_undriven` and `dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint` exercise the production dispatcher with real brokers, NetHosts, echo PTYs, attach requests, controller input, and echoed output. Focused commands: `cargo nextest run -p spt-daemon --test dispatch -E 'test(dispatcher_serves_a_remote_drive_attach_undriven)'` and `cargo nextest run -p spt-daemon --test dispatch -E 'test(dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint)'`. Limitation: the dispatcher is started once; neither test kills/replaces it, cycles a brain, fills/evicts a stream ring, or leaves stale finished attach streams for a successor dispatcher.\n\n5. Adjacent evidence: `crates/spt/tests/attach_wedge_e2e.rs` is the closest real detached-daemon + real `spt rc` process rig, but tests abrupt rc/PTY death and subsequent new attach, not refresh; `crates/spt/tests/brain_respawn_rename.rs` tests a real daemon/binary rename/brain respawn but has no PTY/controller; `crates/spt/tests/brain_survive.rs` tests process-level survival across swapped brain bytes but not production attach redispatch; `crates/spt-daemon/tests/brain_resume_conn_deadlock.rs`, `brain_decouple.rs`, and `conn_blackhole_lifecycle.rs` cover non-draining subscriber, connection retirement, and unrelated-session liveness but not the remote attach dispatcher lifecycle.\n\nCONFIRMED CALL CHAIN AND OWNERSHIP\n\n- PTY data plane: `Broker` owns each `PtySession`, input writer, output ring, controller/viewer table, child PID, and effect journal. `broker.rs::handle_conn` explicitly detaches a dead subscriber but leaves sessions running; NetHost likewise detaches a dead stream subscriber while preserving streams/rings.\n- Controller/viewer connection: `serve_attach` owns the serving side of an rc viewport. Its primary `Brain` connection subscribes both to the peer-initiated NetStream and to the PTY as controller/viewer; a second `wire` Brain connection forwards session output back onto the NetStream to avoid same-connection replay deadlock. Controller input is decoded from `AttachRecord::Input` and delivered with `send_effect_no_ack`; output is encoded as `AttachRecord::Output`.\n- Daemon brain refresh: `cmd_daemon_refresh` calls `refresh_brain`; the supervisor cycles only the brain child. `run_brain` invokes cursor-only `Brain::resume_session_cursors`, intentionally making no PTY subscriptions because the main heartbeat connection is non-draining. Thus `BRAIN_RESUMED` logs session cursors, not rc controller streams. This is correct and orthogonal to rc restoration.\n- NetStream/rc restoration: after `run_brain` reaches its heartbeat, `spawn_net_consumers` starts a fresh dispatcher. `run_dispatch_loop` calls `Brain::net_streams`, selects peer-initiated streams, and spawns `worker`; `worker` calls `peek_first_line(stream_id, from_seq=0)`, classifies the protocol, then creates a fresh Brain and calls `serve_attach`. `serve_attach` replays the Request, resolves endpoint→session, and `attach_as(..., Some(origin_node))` silently retakes the same-origin controller. Therefore the architecture does contain an automatic rc re-serve path; saying the successor only restores `BRAIN_RESUMED` cursors omits this separate dispatcher reconstruction path.\n- Broker lifecycle: a brain-only refresh must not change broker PID, PTY PID, NetHost connection, or NetStream table. A broker restart is a separate lifecycle and severs these ephemeral controller transports; it must not be conflated with brain refresh.\n\nCAUSAL CANDIDATES\n\nA. Stale finished-stream redispatch/takeover — primary candidate, `[INFERENCE]`. `NetStreamInfo` exposes `finished`, but `run_dispatch_loop` selects on only `!initiated_locally`, `next_seq > 0`, and fresh `claimed` membership; it does not exclude finished streams. Every brain generation starts with an empty `claimed` set. Consequently a successor can redispatch historic, already-finished attach streams, replay their old Request records, and transiently/silently retake a same-origin controller while active streams are also being reconstructed. Multiple historical streams can race in HashMap iteration/worker scheduling. This is a direct candidate for partial session recovery and controller churn.\n\nB. Evicted opener makes redispatch one-shot fail — structurally confirmed, causal application `[INFERENCE]`. `NetHost::StreamLog` is a bounded ordinary ring of 4,096 transport-read chunks and evicts its oldest chunks. `run_dispatch_loop` nevertheless reconstructs protocol family exclusively by subscribing from logical zero and parsing the first retained NDJSON line. If the original attach `Request` has rolled out—or the retained floor begins in the middle of an NDJSON record—the first parsable retained record is typically `Input`/`Resize` or invalid JSON. `classify_first_line` deliberately returns `Unknown` for mid-protocol records. The worker exits, but the dispatcher has already inserted the stream ID into `claimed`, so that generation never retries it. Detach+reattach creates a new stream whose Request is again at its retained head, explaining immediate recovery.\n\nC. Partial 4/5 observation: `[INFERENCE]` both candidates naturally produce a non-uniform blast radius. Finished-stream history differs per endpoint, and opener retention depends on the amount/coalescing of operator→target traffic. Four older/high-input streams can have lost their Request or have more stale finished predecessors while one newer/quieter stream retains a classifiable opener and re-serves normally. This is more specific than an undifferentiated PTY freeze: PTY children and broker rings can remain alive while only selected controller forwarding workers fail to reconstruct.\n\nSMALLEST SAFE DETERMINISTIC FEEDBACK LOOP\n\nCompose the `resume_no_control_steal_e2e` real-brain supervisor with the `dispatch.rs` real remote-attach echo flow in a new isolated integration test (suggested name `refresh_rc_stream_recovery_e2e`; avoid `update`/`setup`/`install` in the test-binary name because Windows installer heuristics can cause UAC error 740).\n\nUse a temporary process-global `SPT_HOME`; one in-process target `Broker::bind_in_with_net` with loopback-only `NetHost`; one in-process operator broker; and the real `CARGO_BIN_EXE_spt` brain subprocess under `supervise_brain`. No discovery, relay, production socket, production perch, or live user process is involved. Wire the broker's `BrainRestart` signal exactly as the supervisor tests do, then invoke the private-home `spt daemon refresh` CLI if exact verb coverage is desired; alternatively call `BrainRestart::request` for the narrower brain-replacement loop.\n\nUse N=3 because it is the minimum that can express “most but not all”: spawn three labeled real echo PTYs (`cat` on Unix, `findstr .` on Windows); open three endpoint-addressed Control attach streams; send unique PRE markers and require all three to echo. Preserve the Request in stream 3. For streams 1 and 2, deterministically shape the broker-held peer ring into the natural post-rollover state by calling the test-held target `NetHost::drain_stream(peer_stream_id)`, assert the drained bytes contained the attach Request, then send/observe another input so the retained head is a mid-protocol Input record. This avoids 4,096 timing-sensitive writes or hundreds of MiB of artificial traffic while representing the same observable broker state as bounded-ring rollover.\n\nCycle the real brain and wait for a changed PID/generation plus `BRAIN_NET_CONSUMERS_UP`/a bounded dispatcher-ready condition—not merely `brain.ready`, because current `run_brain` writes ready before its first 500 ms heartbeat starts net consumers. Then send unique POST markers down all three existing controller streams and collect results with bounded `read_event_until` loops. Required pass contract after a fix: all 3 existing streams return their own POST marker exactly once; all PTY PIDs and the broker remain unchanged. Current discriminating failure: stream 3 (retained opener) recovers while streams 1 and 2 time out because successor classification sees Input rather than Request. After collecting that failure state, detach and create fresh attach streams for 1 and 2 and prove RECOVER markers echo; this verifies the reported detach+reattach recovery without masking the original failed assertion.\n\nAdd a separate deterministic selection test for candidate A: construct active and finished peer-initiated attach streams and assert successor claim selection excludes `finished=true` rows before any worker can replay an old Request. Do not rely on concurrent worker ordering to prove this invariant.\n\nThe single end-to-end pass/fail signal should be: `pre_echoes == 3 && broker_pid_same && pty_pids_same && brain_pid_changed && existing_post_echoes == 3`. Diagnostic output should include, per stream: stream ID, `finished`, `next_seq`, whether opener was retained/drained, pre/post echo, and fresh-reattach recovery. This separates PTY survival from controller-stream restoration and makes a failed run actionable.\n\nHISTORICAL RULES\n\n- `CONTEXT.md`, ADR-0004, and ADR-0018 require the stable broker to own PTYs and streams while only the brain is replaceable; routine refresh/update may not terminate or suspend endpoints.\n- `docs/KNOWN-HAZARDS.md` §7.36 covers a suspended/black-holed brain subscriber freezing controller output and requires broker-side stall eviction/recovery; §7.38 requires bounded, cancelable, poison-on-failure physical broker writes. These guards address blocked physical subscriber connections, but do not prove successor dispatch classification/reclaim correctness.\n- `docs/DEBUG-ROLLOUT.md` requires local source-built, isolated-home test tooling and forbids production CLI/debug trust shortcuts. The proposed loop follows that rule: loopback, temp home, scoped PIDs, no shared release/update cache, and no live sessions.","files":[{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Closest real `spt daemon run` + real `spt daemon refresh` E2E. Proves broker/harness/perch survival and brain PID/generation change, but deliberately NET-LESS and has no rc/controller traffic."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Best N-session, real brain-subprocess replacement harness. `spawn_ticker_controller` holds raw spawn-time local controllers and counts output across supervisor restart/promotion; ideal base for a composed dispatcher recovery rig."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Contains `attach_survives_target_brain_restart_exactly_once`, real echo PTY and bidirectional dead-window markers, plus controller/viewer role tests. Successor manually calls `serve_attach`, bypassing dispatcher rediscovery."},{"path":"crates/spt-daemon/tests/dispatch.rs","description":"Production `run_dispatch_loop` integration rig with two loopback brokers. `dispatcher_serves_a_remote_drive_attach_undriven` and endpoint-addressed counterpart prove Request→Input→echo, but do not restart the dispatcher or test retained-ring floors/finished streams."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"Critical reconstruction seam: `run_dispatch_loop`, `worker`, `peek_first_line`, `classify_first_line`, `spawn_dispatcher`. A fresh per-process `claimed` set reclaims peer streams; selection currently does not exclude `finished`, and family recovery depends on retained opener bytes."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-owned NetStream data plane. `StreamLog` has a bounded 4,096-transport-chunk ordinary ring, evicts its front, supports one replaceable subscriber, and preserves stream info across brain subscriber disconnect. `NetStreamInfo.finished` is available to dispatcher selection."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` exact controller/viewer server path. Uses separate receive/controller and wire-forward Brain connections; subscribes the NetStream, replays Request, attaches by origin, journals input, and forwards sequenced PTY output."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"`run_brain` startup and supervisor lifecycle. Uses cursor-only `resume_session_cursors`, writes ready, then starts dispatcher/peer pump on the first net-enabled heartbeat via `spawn_net_consumers`."},{"path":"crates/spt-daemon/src/brain.rs","description":"`resume_session_cursors`/`resume_sessions`, attach APIs, NetStream APIs, and planned brain restart request. Documents why the supervised main brain must not subscribe PTY output on its heartbeat connection."},{"path":"crates/spt-daemon/src/broker.rs","description":"PTY owner and connection cleanup. `handle_conn` detaches dead session/NetStream subscribers but leaves sessions and stream rings alive; `dispatch_spawn`, `dispatch_subscribe`, and `resolve_subscribe` implement spawn-time controller and same-origin silent retake."},{"path":"crates/spt/src/rc.rs","description":"Operator-side production viewport pump. Local attach is a loopback NetStream through the same `serve_attach` dispatcher path as cross-node attach; detach finishes the viewport without killing the broker-owned PTY."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Closest existing real detached daemon + dummy harness + real rc subprocess rig. Covers abrupt rc/PTY failure and later attach, but no brain refresh."},{"path":"crates/spt/tests/brain_respawn_rename.rs","description":"Real daemon process and applied-binary rename/brain respawn path proof; has no hosted PTY/controller assertions."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Deterministic real PTY flood rig proving cursor-only daemon-brain resume avoids request/reply connection self-deadlock; adjacent subscriber hazard, not remote attach redispatch."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"Real broker/PTY controller black-hole and physical connection retirement evidence for KNOWN-HAZARDS §7.36/§7.38; no refresh or dispatcher reconstruction."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Windows deterministic black-holed-controller lifecycle harness; proves unrelated sessions continue and bad physical conn retires, but not brain refresh."},{"path":"docs/KNOWN-HAZARDS.md","description":"Historical controller/brain subscriber hazards, especially §7.36 broker-viewer-brain decoupling and §7.38 bounded/cancelable physical writes; useful to distinguish prior blocked-connection failures from reconstruction failures."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Requires isolated source-built debug validation rather than production update/debug surfaces; supports temp-home, loopback-only feedback loop."},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","description":"Original stable-broker/restartable-brain and no-endpoint-termination update contract."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Restored process boundary: broker owns PTYs/sockets/NetHost; brain-owned consumers are reconstructed after replacement."}],"architecture":"The broker is the durable-in-process data plane for a daemon lifetime: it owns PTY masters/children, output logs, input effects, NetHost connections, and NetStream rings. `spt rc` does not own the PTY; it owns an operator viewport carried by a broker-held stream. On the target node, a brain-process dispatcher classifies that peer stream and launches a `serve_attach` worker whose broker IPC subscriptions temporarily bind the stream to the PTY controller/viewer slot. A brain refresh kills these control-plane workers but must leave the broker data plane untouched. The successor main brain deliberately restores only session cursors on its non-draining heartbeat connection; independently, its newly spawned dispatcher must rediscover and re-serve every still-active peer stream. Existing tests prove each segment separately but not their composition. The uncovered join is successor stream claim/classification: finished streams are not filtered, and active stream family is re-derived from a bounded byte ring whose opener may no longer exist. The proposed N=3 isolated supervisor+dispatcher+echo-PTY test makes that join deterministic and distinguishes session survival, active controller reconstruction, and detach/reattach recovery."},"status":"success"},"isError":false,"timestamp":1784196313871}}
{"type":"message","id":"0a7921b4","parentId":"1b452802","timestamp":"2026-07-16T10:05:13.886Z","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":1784196313882}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `RefreshLifecycle`:\n\nConfirmed and included in my finalized lifecycle report: 4096-chunk opener eviction actually first trips Brain's cursor-0 forward-gap rejection; worker becomes Failed (or Unknown on partial/non-gap variants), and pre-spawn claimed insertion prevents retry. Sent exact refs to Main.\n\nAn agent sent this while you were waiting or working. Any active interruptible wait was stopped early so you can read it now.\n\nIf a response is expected, reply with the `irc` tool (`op: \"send\"`, `to: \"RefreshLifecycle\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"1531e60abe9c5ea4","from":"RefreshLifecycle","message":"Confirmed and included in my finalized lifecycle report: 4096-chunk opener eviction actually first trips Brain's cursor-0 forward-gap rejection; worker becomes Failed (or Unknown on partial/non-gap variants), and pre-spawn claimed insertion prevents retry. Sent exact refs to Main."},"attribution":"agent","id":"2861a342","parentId":"0a7921b4","timestamp":"2026-07-16T10:05:13.889Z"}
{"type":"message","id":"e50c7242","parentId":"2861a342","timestamp":"2026-07-16T10:05:26.062Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_0c2bdc083e8612c5016a58ace194c8819bb635c6367cc2a361\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":53709,"output":4,"cacheRead":78336,"cacheWrite":0,"totalTokens":132049,"cost":{"input":0.26854500000000003,"output":0.00012,"cacheRead":0.039168,"cacheWrite":0,"total":0.307833}},"stopReason":"stop","timestamp":1784196313896,"responseId":"resp_0c2bdc083e8612c5016a58acda9168819bb017cbc89785720a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_0c2bdc083e8612c5016a58ace194c8819bb635c6367cc2a361","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":12164.854399999604,"ttft":7670.496399999596,"contextSnapshot":{"promptTokens":132045,"nonMessageTokens":15087}}}
{"type":"message","id":"b6ff54e7","parentId":"2861a342","timestamp":"2026-07-16T10:05:43.333Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Correcting cursor handling before yielding**"},{"type":"toolCall","id":"call_Tj6uwG5mmBRZooKO0SrORkXo|fc_09e27ea08b6310f7016a58acede508819896f3a92b7300cfbe","name":"irc","arguments":{"i":"Correcting opener eviction mechanism","op":"send","to":"Main","message":"RefreshLifecycle confirmed nuance to my adjacent opener-eviction candidate: when retained ring floor >0, `peek_first_line`'s fresh Brain subscribed at cursor 0 first rejects the forward NetStream seq gap, so worker ends `Failed` before classification. `Unknown` applies partial/mid-record variants that do not trip that exact gap path. Pre-spawn `claimed.insert` still makes either failure one-shot/no-retry.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3562,"output":215,"cacheRead":128512,"cacheWrite":0,"totalTokens":132289,"reasoningTokens":72,"cost":{"input":0.017810000000000003,"output":0.00645,"cacheRead":0.064256,"cacheWrite":0,"total":0.088516}},"stopReason":"toolUse","timestamp":1784196326081,"responseId":"resp_09e27ea08b6310f7016a58ace709048198b64896466c269c12","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_09e27ea08b6310f7016a58acebb65881988c9e3c383b3e56a1","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKztU6skbU-oSkSeefUxahdvmRrDGYI6df8zytMfZzAhzKrW8eaIgy07JQzkoLQL_dFxQbJARxi5bhqjTS6Hq0-CtcLllCdPSqu9KNAQAOY5mcOUIHmGrM8rffQZuczMSi5R2TisoHaSSt8qmY8iInnkoaM_YF5fO7IFsOdbRJtCp9N7veLysibaqtxrYdEE2DOyCz8lqJ784KZsRXM82JN43oqKYDpWdHXWNZbdxeIvETLZqr0r0mXoTAOPgx-RkGXSZE-YgX6COKGA3VI_I_QtgC8oXynKpaJwTZ8CuSA_sGv6NJLpzwGUGQGPRZOQid0JoAbGuMoyZsVRNWTDS2avT_By-jlvmEu9mmJefQsqKTD1DCZwRm2EUysgVwMEiPgOYT_IMB648c3dlpH3WC8fsi76fFA83dmkMKEx8ZOd7l1-lCNQBKKG8V7dxERlIhVE4mFERhbMZW2b3qaTfa3dizpxwxl76FFPu74RrIbAftUNUE9Q8XM7WvNsAbAI2crCtpYCZy9VJY4QmGnUjEr6vxC1k1K5mE6FWnTe6NCl7YOiSV5EEyR8rYe6jagJabS124pQ6OtjS8wTgxBOSU52TB8vKIu6SrIfjDNA7xL_E5lP42EauivdzB-4z-KZZtIqEJ8VGfNb5co0poH1YDUF6psvqx_J4Yu-tJQRjJYfUwJTBVZyJuPYk2yCKH1TdYg_XaItJJV1v3HMQ1ISVskuXTc5J5KFPBRW8rGcDmu5IybhHLdprsTdGEaGTcY9aSpOF5JYQ8YHgfzGODznzewlaYbgQHldwLan4WvT05gdxj3txl6uXO7QN6C0snr5Q_K5BhcU95V5KJHL7pZOd2AeZ227g23wHpIfkz1NfbRtu9WyCFKGTGhknrlMUuTKYfHkMi30524dwl3XbzL1agWzY2ITtFeFy7g1k48gwfefFVGc3NST6I2f5_AqrjxbOo165Dx-s1b75abKKOHhKizwjJllkF31K156cjYUYY_yuMwbRpah8WnoM65we07IXoxb4cg7dgNvgFCSZ05mfq6E5B9erG_K6zCO4ZWyvbbw2FgPkLwIvC0DmOkEmX3aKD9EliDli3ZN0wVFrg-SwmuuB1sonur7aPdbaCQFJBORheuvaVPCIp4GmE1zEY1e6phDsQaY8-zpFF3fxKZURlPcsGrE3o8g04Crns0yZDohs3GWsVLTUuH9zylAUWCuJrWRse-tq7xOiDqBFqRpnhbFU8q15QMXm6ciH_JaZ8IhqBfLtQBi87ZlmQEj1Zhmn0fH-liYRLzYJ3wO8LPmoMUhZXeyMut1FpsqOuKSyfsAGblLw-bj6kjR8fZjQiGTSz3HOSm2umgzL4-xnOTgjkHgzRT4-K2Qm73ieLSfvvkQYCt3mzpi2D6HdfNTWI4_FwiwdQxOGqyqnHE3HnNTvN3rGg==","summary":[{"type":"summary_text","text":"**Correcting cursor handling before yielding**"}]},{"id":"fc_09e27ea08b6310f7016a58acede508819896f3a92b7300cfbe","type":"function_call","status":"completed","arguments":"{\"i\":\"Correcting opener eviction mechanism\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"RefreshLifecycle confirmed nuance to my adjacent opener-eviction candidate: when retained ring floor >0, `peek_first_line`'s fresh Brain subscribed at cursor 0 first rejects the forward NetStream seq gap, so worker ends `Failed` before classification. `Unknown` applies partial/mid-record variants that do not trip that exact gap path. Pre-spawn `claimed.insert` still makes either failure one-shot/no-retry.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_Tj6uwG5mmBRZooKO0SrORkXo","name":"irc"}]},"duration":17250.27400000021,"ttft":5631.66849999968,"contextSnapshot":{"promptTokens":132074,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Tj6uwG5mmBRZooKO0SrORkXo|fc_09e27ea08b6310f7016a58acede508819896f3a92b7300cfbe","toolName":"irc","startedAt":"2026-07-16T10:05:43.334Z","intent":"Correcting opener eviction mechanism"},"id":"91901e4c","parentId":"b6ff54e7","timestamp":"2026-07-16T10:05:43.334Z"}
{"type":"message","id":"8ce7bb65","parentId":"91901e4c","timestamp":"2026-07-16T10:05:43.335Z","message":{"role":"toolResult","toolCallId":"call_Tj6uwG5mmBRZooKO0SrORkXo|fc_09e27ea08b6310f7016a58acede508819896f3a92b7300cfbe","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: woken"}],"details":{"op":"send","from":"ReproHarness","to":"Main","receipts":[{"to":"Main","outcome":"woken"}]},"isError":false,"timestamp":1784196343335}}
{"type":"message","id":"5d6d26a2","parentId":"8ce7bb65","timestamp":"2026-07-16T10:06:17.285Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Designing final yield schema**"},{"type":"toolCall","id":"call_aUH2TW598OfjHyoqMRvlD2mt|fc_09e27ea08b6310f7016a58ad193cf48198bd194a17ebc8a06e","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Correction/finalization to the submitted investigation: the bounded-ring opener-loss candidate has an earlier failure mode than classification. When a successor dispatcher calls `peek_first_line` and subscribes at NetStream cursor 0 after the 4,096-chunk ring has evicted its opener, the first retained transport frame has sequence >0. `Brain::read_event` rejects that forward gap, so `worker` normally ends as `DispatchOutcome::Failed` before `classify_first_line` runs. `DispatchOutcome::Unknown` remains possible where replay begins at sequence 0 but the bytes are partial/mid-protocol/unparseable. In both variants, `run_dispatch_loop` inserted the stream ID into its generation-local `claimed` set before spawning the worker, so the failure is permanent for that brain generation and detach+reattach heals by creating a fresh stream. All other findings, harness recommendations, commands, limitations, and the N=3 deterministic feedback loop remain unchanged. No commands, refreshes, updates, edits, or live-session actions were performed.","files":[{"path":"crates/spt-daemon/src/dispatch.rs","description":"`run_dispatch_loop` claims before worker success; `peek_first_line` subscribes from cursor 0. A failed/unknown worker is not retried during that brain generation."},{"path":"crates/spt-daemon/src/brain.rs","description":"NetStream event cursor validation rejects a forward sequence gap before dispatcher classification when the retained stream floor is above zero."},{"path":"crates/spt-daemon/src/nethost.rs","description":"`StreamLog` ordinary streams retain only 4,096 transport-read chunks and evict oldest chunks, allowing the retained floor to advance beyond the attach opener."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Closest real daemon-refresh process harness, but NET-LESS and without rc/attach traffic."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Closest N-controller real-brain replacement harness; recommended base for composing refresh with dispatcher recovery."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Bidirectional attach brain-restart proof manually re-serves a known stream and therefore bypasses rediscovery/cursor-zero peek failure."},{"path":"crates/spt-daemon/tests/dispatch.rs","description":"Production dispatcher plus real echo PTY/input/output coverage, but no dispatcher restart, ring-floor advance, or finished-stream successor claim."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` binds a rediscovered NetStream to the PTY controller/viewer and forwards bidirectional traffic using split broker IPC connections."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Successor brain restores PTY cursors separately and starts fresh net consumers/dispatcher on its first net-enabled heartbeat."},{"path":"crates/spt-daemon/src/broker.rs","description":"Stable data-plane owner for PTYs, controller/viewer truth, and connection cleanup across brain replacement."},{"path":"docs/KNOWN-HAZARDS.md","description":"Historical blocked-subscriber/controller hazards §7.36 and §7.38; adjacent but not coverage for successor stream rediscovery."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Requires isolated source-built validation, satisfied by the proposed temp-home loopback-only rig."}],"architecture":"Stable broker ownership and restartable brain ownership remain distinct: broker retains PTYs, NetHost connections, and NetStream rings; brain-process dispatcher workers temporarily subscribe those retained streams and attach them to PTY controller/viewer slots. Refresh kills workers, then the successor dispatcher reconstructs them. The uncovered join has two causal candidates: `[INFERENCE]` finished streams are not excluded from successor claims and can replay stale attach requests; and retained active streams whose floor advanced beyond cursor 0 fail the successor's cursor-zero peek before protocol classification. Because claim insertion precedes worker success, neither failure retries until another brain generation, while rc detach+reattach creates a clean stream. The smallest deterministic regression loop remains N=3 real echo PTYs under an isolated real brain supervisor: preserve one opener, use the test-held NetHost `drain_stream` seam to shape two streams into post-rollover state, refresh, and require all three existing streams to round-trip unique post-refresh markers while broker/PTY PIDs remain stable; then prove fresh reattach recovers any failed streams for diagnostic discrimination."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3800,"output":1012,"cacheRead":128512,"cacheWrite":0,"totalTokens":133324,"reasoningTokens":56,"cost":{"input":0.019000000000000003,"output":0.03036,"cacheRead":0.064256,"cacheWrite":0,"total":0.113616}},"stopReason":"toolUse","timestamp":1784196343391,"responseId":"resp_09e27ea08b6310f7016a58acf800b081988db9909920ffc44e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_09e27ea08b6310f7016a58acfc5bc08198a8b6993f9553ec22","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWK0ZEh3-pc8LKYOwQmucEQV0ukOQUkxHCOJ5k5Dm_j8L2Tw1Q61EP8YONqI9nfZ_PjfEb9u_ImuMt9Mh-ASwM9rs4sQyf3rw_aPWSuaAIwGSHzqxkb1liMLfoP3bfOOhlC_Ky4Y0b6azlIqwEnYaNgrTZpo359TVOy190xd_lbfa3MqrWOw5BMcgHo-WuDfExaejXfPR79J1SOmpb94grLTh0cag95Gd8G2AjgqGvyAaMC83toiRg4drKtXgPLZ5AUTgUFyodAa5X3sAHPz0mWutMtuhUXnvAnYKxM_xNpELHRjudRJ3P5PShrLKu8zmrDovBMOyV6EnuGsi2OjS2afSM9qvvxGYE-c0IViQ9_eSXREd8L1u1CmuGvo95y2d-CfQwxfOf3HPIeMA-SehQ-XDsDjHfygKg34_2gQG_MiZRzOIpj5Kie5jDbwcQ_N2ToZ95frzNOb2R3O46Z6RAE-p-OiB579tM4qEHTR3yNAuH5uaTiZSbLPOLt123fnBxhkOS2xxmKGUXkROsK0sJ1yD-jS6lSuewQnvE-BIqCCVAo2WzvXF8jOU2FA86l5utk0uJtcBpTcqpd1GemYV8VE6sNS9jKT48JjIQk_ASOXoDCzADxlML_UVvsXCURX20fhc7GR4jU5h3fZ9ayCjnZ59UyfgMlGnxum9nImqWsg9W0NM1VVvZs0O5wHTg0Yc9tb57QJTxybFkD8iFbLVnoWzR6AQCH0nhhHBxLy2hjrRRXlVSAGGh8sxz56vz7kDH7IJb1U1VS3m3OGGMDO_Sq0GGSLelIcLRTHIidLvEwCVA31dkexDr-Sipn7C6Ieix7zfj7pduq9DDdykESZe2XLSNz3cQNXpk1BsyNcmjd3hrD4oQXWLB_-jABZjuKunsU1gwu-QwM6OMiziSQP5RBNeoUh9eJ_u8YSoImpUaxfiyywY89ehF9A7uMEy96CQRJvk54dqG4Dh-9O_xZ6gJAXoOyONNpIg06zhxKZpCWVF1krGlFSqQcrkl91HZSBxmZBHWGBVwKxb-hvzLexrpF84NKff61xYyYYUq0jxHPO4CM792t09EhwZ3LG_xmY3nxfbNcV17pXxggCvItku6ZtxWv6nV2AH7cv9G2nQq0rrjjl1M6wvf-0uoxN03dnacknk0foNCg28tr-UtWZkyZbfUVa7swOw989EKKvafbPlrXleOWQNX5g-EkDBTu-JEo9rNUrhI_ieWGNluybz_S1vL2--aohutKm9-Lij7xmOPaY-3aW528fszYvVDNHl6mAuJEjJVMqdyfL1i-ZCpZ8l7xTx74970ThNZ-NPJqewcF384Ca1Bxy7aHp0aJo1DFOn","summary":[{"type":"summary_text","text":"**Designing final yield schema**"}]},{"id":"fc_09e27ea08b6310f7016a58ad193cf48198bd194a17ebc8a06e","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Correction/finalization to the submitted investigation: the bounded-ring opener-loss candidate has an earlier failure mode than classification. When a successor dispatcher calls `peek_first_line` and subscribes at NetStream cursor 0 after the 4,096-chunk ring has evicted its opener, the first retained transport frame has sequence >0. `Brain::read_event` rejects that forward gap, so `worker` normally ends as `DispatchOutcome::Failed` before `classify_first_line` runs. `DispatchOutcome::Unknown` remains possible where replay begins at sequence 0 but the bytes are partial/mid-protocol/unparseable. In both variants, `run_dispatch_loop` inserted the stream ID into its generation-local `claimed` set before spawning the worker, so the failure is permanent for that brain generation and detach+reattach heals by creating a fresh stream. All other findings, harness recommendations, commands, limitations, and the N=3 deterministic feedback loop remain unchanged. No commands, refreshes, updates, edits, or live-session actions were performed.\",\"files\":[{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"`run_dispatch_loop` claims before worker success; `peek_first_line` subscribes from cursor 0. A failed/unknown worker is not retried during that brain generation.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"NetStream event cursor validation rejects a forward sequence gap before dispatcher classification when the retained stream floor is above zero.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"`StreamLog` ordinary streams retain only 4,096 transport-read chunks and evict oldest chunks, allowing the retained floor to advance beyond the attach opener.\"},{\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"description\":\"Closest real daemon-refresh process harness, but NET-LESS and without rc/attach traffic.\"},{\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"description\":\"Closest N-controller real-brain replacement harness; recommended base for composing refresh with dispatcher recovery.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"Bidirectional attach brain-restart proof manually re-serves a known stream and therefore bypasses rediscovery/cursor-zero peek failure.\"},{\"path\":\"crates/spt-daemon/tests/dispatch.rs\",\"description\":\"Production dispatcher plus real echo PTY/input/output coverage, but no dispatcher restart, ring-floor advance, or finished-stream successor claim.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"`serve_attach` binds a rediscovered NetStream to the PTY controller/viewer and forwards bidirectional traffic using split broker IPC connections.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"Successor brain restores PTY cursors separately and starts fresh net consumers/dispatcher on its first net-enabled heartbeat.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"Stable data-plane owner for PTYs, controller/viewer truth, and connection cleanup across brain replacement.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Historical blocked-subscriber/controller hazards §7.36 and §7.38; adjacent but not coverage for successor stream rediscovery.\"},{\"path\":\"docs/DEBUG-ROLLOUT.md\",\"description\":\"Requires isolated source-built validation, satisfied by the proposed temp-home loopback-only rig.\"}],\"architecture\":\"Stable broker ownership and restartable brain ownership remain distinct: broker retains PTYs, NetHost connections, and NetStream rings; brain-process dispatcher workers temporarily subscribe those retained streams and attach them to PTY controller/viewer slots. Refresh kills workers, then the successor dispatcher reconstructs them. The uncovered join has two causal candidates: `[INFERENCE]` finished streams are not excluded from successor claims and can replay stale attach requests; and retained active streams whose floor advanced beyond cursor 0 fail the successor's cursor-zero peek before protocol classification. Because claim insertion precedes worker success, neither failure retries until another brain generation, while rc detach+reattach creates a clean stream. The smallest deterministic regression loop remains N=3 real echo PTYs under an isolated real brain supervisor: preserve one opener, use the test-held NetHost `drain_stream` seam to shape two streams into post-rollover state, refresh, and require all three existing streams to round-trip unique post-refresh markers while broker/PTY PIDs remain stable; then prove fresh reattach recovers any failed streams for diagnostic discrimination.\"}}}","call_id":"call_aUH2TW598OfjHyoqMRvlD2mt","name":"yield"}]},"duration":33892.558400000446,"ttft":4945.6194000002,"contextSnapshot":{"promptTokens":132312,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_aUH2TW598OfjHyoqMRvlD2mt|fc_09e27ea08b6310f7016a58ad193cf48198bd194a17ebc8a06e","toolName":"yield","startedAt":"2026-07-16T10:06:17.285Z"},"id":"24b9650e","parentId":"5d6d26a2","timestamp":"2026-07-16T10:06:17.285Z"}
{"type":"message","id":"b47b343d","parentId":"24b9650e","timestamp":"2026-07-16T10:06:17.286Z","message":{"role":"toolResult","toolCallId":"call_aUH2TW598OfjHyoqMRvlD2mt|fc_09e27ea08b6310f7016a58ad193cf48198bd194a17ebc8a06e","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Correction/finalization to the submitted investigation: the bounded-ring opener-loss candidate has an earlier failure mode than classification. When a successor dispatcher calls `peek_first_line` and subscribes at NetStream cursor 0 after the 4,096-chunk ring has evicted its opener, the first retained transport frame has sequence >0. `Brain::read_event` rejects that forward gap, so `worker` normally ends as `DispatchOutcome::Failed` before `classify_first_line` runs. `DispatchOutcome::Unknown` remains possible where replay begins at sequence 0 but the bytes are partial/mid-protocol/unparseable. In both variants, `run_dispatch_loop` inserted the stream ID into its generation-local `claimed` set before spawning the worker, so the failure is permanent for that brain generation and detach+reattach heals by creating a fresh stream. All other findings, harness recommendations, commands, limitations, and the N=3 deterministic feedback loop remain unchanged. No commands, refreshes, updates, edits, or live-session actions were performed.","files":[{"path":"crates/spt-daemon/src/dispatch.rs","description":"`run_dispatch_loop` claims before worker success; `peek_first_line` subscribes from cursor 0. A failed/unknown worker is not retried during that brain generation."},{"path":"crates/spt-daemon/src/brain.rs","description":"NetStream event cursor validation rejects a forward sequence gap before dispatcher classification when the retained stream floor is above zero."},{"path":"crates/spt-daemon/src/nethost.rs","description":"`StreamLog` ordinary streams retain only 4,096 transport-read chunks and evict oldest chunks, allowing the retained floor to advance beyond the attach opener."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Closest real daemon-refresh process harness, but NET-LESS and without rc/attach traffic."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Closest N-controller real-brain replacement harness; recommended base for composing refresh with dispatcher recovery."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Bidirectional attach brain-restart proof manually re-serves a known stream and therefore bypasses rediscovery/cursor-zero peek failure."},{"path":"crates/spt-daemon/tests/dispatch.rs","description":"Production dispatcher plus real echo PTY/input/output coverage, but no dispatcher restart, ring-floor advance, or finished-stream successor claim."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` binds a rediscovered NetStream to the PTY controller/viewer and forwards bidirectional traffic using split broker IPC connections."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Successor brain restores PTY cursors separately and starts fresh net consumers/dispatcher on its first net-enabled heartbeat."},{"path":"crates/spt-daemon/src/broker.rs","description":"Stable data-plane owner for PTYs, controller/viewer truth, and connection cleanup across brain replacement."},{"path":"docs/KNOWN-HAZARDS.md","description":"Historical blocked-subscriber/controller hazards §7.36 and §7.38; adjacent but not coverage for successor stream rediscovery."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Requires isolated source-built validation, satisfied by the proposed temp-home loopback-only rig."}],"architecture":"Stable broker ownership and restartable brain ownership remain distinct: broker retains PTYs, NetHost connections, and NetStream rings; brain-process dispatcher workers temporarily subscribe those retained streams and attach them to PTY controller/viewer slots. Refresh kills workers, then the successor dispatcher reconstructs them. The uncovered join has two causal candidates: `[INFERENCE]` finished streams are not excluded from successor claims and can replay stale attach requests; and retained active streams whose floor advanced beyond cursor 0 fail the successor's cursor-zero peek before protocol classification. Because claim insertion precedes worker success, neither failure retries until another brain generation, while rc detach+reattach creates a clean stream. The smallest deterministic regression loop remains N=3 real echo PTYs under an isolated real brain supervisor: preserve one opener, use the test-held NetHost `drain_stream` seam to shape two streams into post-rollover state, refresh, and require all three existing streams to round-trip unique post-refresh markers while broker/PTY PIDs remain stable; then prove fresh reattach recovers any failed streams for diagnostic discrimination."},"status":"success","type":"result"},"isError":false,"timestamp":1784196377285}}
{"type":"message","id":"9bb1107e","parentId":"b47b343d","timestamp":"2026-07-16T10:06:22.917Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_09e27ea08b6310f7016a58ad1dfd648198a3fd71db9e1051cc\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4826,"output":4,"cacheRead":128512,"cacheWrite":0,"totalTokens":133342,"cost":{"input":0.024130000000000002,"output":0.00012,"cacheRead":0.064256,"cacheWrite":0,"total":0.088506}},"stopReason":"stop","timestamp":1784196377298,"responseId":"resp_09e27ea08b6310f7016a58ad19cf8081989e9acace30bd6098","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_09e27ea08b6310f7016a58ad1dfd648198a3fd71db9e1051cc","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":5618.81240000017,"ttft":4668.544400000013,"contextSnapshot":{"promptTokens":133338,"nonMessageTokens":15087}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-16T10:13:22.919Z"},"id":"6df90403","parentId":"9bb1107e","timestamp":"2026-07-16T10:13:22.919Z"}
