{"type":"title","v":1,"title":"","updatedAt":"2026-07-16T09:55:28.612Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f6a5a-44e6-7001-a0d0-245da84c5de1","timestamp":"2026-07-16T09:55:28.612Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"bcda0558","parentId":null,"timestamp":"2026-07-16T09:55:28.644Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"5b2b0e3c","parentId":"bcda0558","timestamp":"2026-07-16T09:55:28.644Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"f562e350","parentId":"5b2b0e3c","timestamp":"2026-07-16T09:55:28.648Z","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: **Daemon Lifecycle 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 `RefreshLifecycle`. Currently visible peers:\n- `Main` — main (main, running)\n- `PtyControl` — PTY Transport Investigator (sub, running)\n- `ReproHarness` — Integration Test Investigator (sub, running)\n\nUse `irc` only for quick coordination, never long-form content. Address peers by id or use `\"all\"` to broadcast.\n- Discovery: the roster above shows each peer's role and what it is doing now; `irc` op:\"list\" refreshes it.\n- Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide.\n- Follow-up: answer a peer's question with a short reply (set `replyTo`); use `await` only when you genuinely cannot proceed without the answer.\n\nCOMPLETION\n===================================\n\nNo TODO tracking, no progress updates. Execute; report results with `yield`.\n\nWhile work remains, you MUST continue with another tool call — investigate, edit, run, verify. Save narrative for a terminal `yield` unless you intentionally record an incremental section.\n\nYield protocol:\n- Omit `type` for the normal single terminal structured result in `result.data`.\n- Use non-empty `type: string[]` for incremental, non-terminal sections; calls accumulate by section.\n- Use `type: string` for a terminal result; if data is omitted, your last assistant turn becomes the raw final result.\n\nThis is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `result.data`.\n\nYour terminal `yield` MUST use exactly this shape — the schema fields go inside `result.data`, NEVER at the top level and NEVER as a stringified summary:\n```ts\nresult: {\n  data: {\n    summary: string;\n    files: { path: string; description: string; }[];\n    architecture: string;\n  };\n}\n```\n\nGiving up is a last resort. If truly blocked, you MUST terminal-yield `result.error` describing what you tried and the exact blocker.\nYou NEVER give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.\n\nYou MUST keep going until this ticket is closed. This matters.\n\nPROJECT\n===================================\n\n<workstation>\n- OS: win32 10.0.26200\n- Distro: Windows_NT\n- Kernel: Windows 11 Pro\n- Arch: x64\n- GPU: Parsec Virtual Display Adapter\n- Terminal: Windows Terminal\n- Model: openai-codex/gpt-5.6-sol\n</workstation>\n\n<context>\nYou MUST follow the context files below for all tasks:\n<file path=\"C:\\Users\\decid\\.claude\\CLAUDE.md\">\n## Reference Documents\n\nThe `~/.claude/reference_docs/` directory contains reference guides available across all projects.\n\n- **[claude-code-hooks.md](reference_docs/claude-code-hooks.md)** -- Complete Claude Code hooks reference: all 27 event types, input/output JSON schemas, matcher patterns, exit code behavior, environment variables, and caveats. Consult this when building or debugging hooks.\n</file>\n</context>\nThe context files above are loaded automatically. You NEVER `grep`/`glob` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.\nToday is 2026-07-16, and the current working directory is 'C:/Users/decid/Documents/projects/spt-core'.\n\n<critical>\n- Each response MUST advance the task. There is no stopping condition other than completion.\n- You MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- You MUST verify the effect of significant behavioral changes before yielding: run the specific test, command, or scenario that covers your change.\n</critical>","task":"Complete the assignment below, thoroughly:\n\n# Target\nDaemon refresh/update implementation and process-lifecycle boundaries; exact symbols and call chain. Non-goal: general update review.\n# Change\nTrace `spt daemon refresh` and `spt update` through CLI, service manager, daemon brain, broker, inherited handles, and restart/refresh behavior. Identify operations capable of invalidating or pausing existing PTY controller streams while leaving broker PTYs alive. Rank falsifiable candidates with evidence.\n# Acceptance\nReport exact line-anchored symbols, process transitions, likely race windows, and existing focused tests or missing test seams. No writes or commands that alter runtime state.","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":"cdbc830c","parentId":"f562e350","timestamp":"2026-07-16T09:55:28.656Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\nDaemon refresh/update implementation and process-lifecycle boundaries; exact symbols and call chain. Non-goal: general update review.\n# Change\nTrace `spt daemon refresh` and `spt update` through CLI, service manager, daemon brain, broker, inherited handles, and restart/refresh behavior. Identify operations capable of invalidating or pausing existing PTY controller streams while leaving broker PTYs alive. Rank falsifiable candidates with evidence.\n# Acceptance\nReport exact line-anchored symbols, process transitions, likely race windows, and existing focused tests or missing test seams. No writes or commands that alter runtime state."}],"attribution":"agent","timestamp":1784195728650}}
{"type":"message","id":"181b3ff9","parentId":"cdbc830c","timestamp":"2026-07-16T09:55:36.409Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning skill and doc reads**"},{"type":"toolCall","id":"call_HCYtxM7YM7vS5XzadyHRPDFA|fc_0cc7a0c9a797f232016a58aa985970819ab9656292398d2639","name":"read","arguments":{"i":"Reading diagnosis workflow","path":"skill://diagnose","selector":null}},{"type":"toolCall","id":"call_Y1Hxk0fwkL33I2w9fhigUFBg|fc_0cc7a0c9a797f232016a58aa985988819ab311a2678929ef51","name":"glob","arguments":{"i":"Mapping lifecycle sources","path":"**/*.{ts,tsx,js,mjs,cjs,md,json}","hidden":true,"gitignore":true,"limit":500}},{"type":"toolCall","id":"call_rgzsCOMS8u8nHxMIVq6rKAZE|fc_0cc7a0c9a797f232016a58aa985994819a8bca666bf84a1e2e","name":"grep","arguments":{"i":"Locating refresh update paths","pattern":"daemon refresh|\\brefresh\\b|\\bupdate\\b|restart|broker|brain","path":"src;packages;test;tests;docs;CONTEXT.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_3pZmXCuOduphweSgbvs6krJX|fc_0cc7a0c9a797f232016a58aa98599c819aa379f74978b45ace","name":"read","arguments":{"i":"Reading domain context","path":"CONTEXT.md","selector":null}},{"type":"toolCall","id":"call_msUQmsgOctlX1F5N2SyLaHZ6|fc_0cc7a0c9a797f232016a58aa9859a4819a9eb7710de6a124bc","name":"read","arguments":{"i":"Reading known hazards","path":"docs/KNOWN-HAZARDS.md","selector":null}},{"type":"toolCall","id":"call_NErsR1cD7pN3wV7cXZBoomFS|fc_0cc7a0c9a797f232016a58aa9859d8819a88241bcdaed6db94","name":"read","arguments":{"i":"Reading rollout guidance","path":"docs/DEBUG-ROLLOUT.md","selector":null}},{"type":"toolCall","id":"call_XnX2rK1jL04iKinxxGde6WU4|fc_0cc7a0c9a797f232016a58aa9859e0819a9d2fddd2371a28e4","name":"glob","arguments":{"i":"Finding architecture decisions","path":"docs/adr/**/*.md","hidden":true,"gitignore":true,"limit":200}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14541,"output":342,"cacheRead":0,"cacheWrite":0,"totalTokens":14883,"reasoningTokens":52,"cost":{"input":0.072705,"output":0.01026,"cacheRead":0,"cacheWrite":0,"total":0.08296500000000001}},"stopReason":"toolUse","timestamp":1784195728709,"responseId":"resp_0cc7a0c9a797f232016a58aa913724819a839b9d70d0f4e674","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cc7a0c9a797f232016a58aa922dc8819a8e196af6b1991b9a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqYeKkSs6HEW09BlKFBLyDIVJtdvszL3hfmMBpXS8jFCtugX1iEWdr3tpqPjNfrvBOq-EO02YnMm_9Gg7TdGb2iqtPCK3vNzdfJkk6JkIqcv2zjq19LY52mFpey6riXBtvHGKj7V6HXSFJw9VLHSoxhaiAXg5_H7YHWjzxEToLzK_9VYkCcWGvZOKaxBlAJPaY1hWd7LPUD4QnP4gpvJlSgNjnQi5-0HS6g2wgBXm7w1DzD8nay7eGf5SlHXTWuviKh9looKvt5MdeSm6TidtpHcw78KPXcsK56RVUOiYyZ7Wxp2VE61c9jLi2nll7JQbtBe0IqRp2UQpNztP6MVQcJ0_hzKCrnXtrYgteYgyFYuteV8TUvni22f6itNbV6YV-Akhq5EgRhQuwJcg1fUP92UrGDxFZTofgMPQrfJgqBon_vKC-BZ7veAXt8RwozENS6VEFMWLyi2n8PKXoArHBXGKLPqjgjjHMEl96cxb1rDM01cfX2q--vHo12X51Y9rRDPWjPv60prc55wNAAaaNrog45Srt7LOYo_8LLeQM-VryQu0AhcSz72eXz5SyPkwJccrHLIdhsvzm4DuyiO1GfM6jqN4QOYcgHUecQPR2j1B3IsRk_RTGFRcqlMInxa64JxPxyhmwKQk4r7HZ_UckfTAuDSbt8x3pU_krI_-x5CVPtZhFCmDE_o3uGn9Qew4dQHyaD9drY2KTTJ7vWqfMJVsHoSsSDfnuDyMzdqkVaRxm447_YLvbtRg1vNWgRypBvWaDymbYlhAqOCFevc4akO0xvAuXbQebX3ng11FQQkIn8krw7OJe4bviG9aaxZ_q-T8UMY21VGWNFGk76i_1vyQmWzhirCREfNwkPCjJ-yfBn-yrr_qUAUeigF-F5Cr0KjBWnzDXreQiNwlXKpVpI-Vg9zSTRBbMb6KDX46slWF8NtDrWtSza49lFyui13cO_01k4pSZ1lOHamHlOGh1ZIsa6g3O6j34Vg-PIu-kVoeUPGH3OQ3-5Iiq2kbHwxWyLuWwpD5t8Mh_A82z_U7y73YYA_79w76wr8zukz7Ep1I8s8O4xkcqMF-H0Pn_iGWKo-0DkeTCBV11ZNsSryXHvIxt1IdK65Cx1w498Q6qK0TLuXLlrXWu9OTVVQwRnudpwY3xN7N6AfBFuqv-x5_2r79P73RezaCVtohzNqmGx9lORwyAmbfv6wwA3OOekys8DGJ3v5Q4EMHWTkNto3LDWs-U3pnlW3_YdPM-t1TchIVEDhvtknQ6wjwxX-ZF5XgNt","summary":[{"type":"summary_text","text":"**Planning skill and doc reads**"}]},{"id":"fc_0cc7a0c9a797f232016a58aa985970819ab9656292398d2639","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading diagnosis workflow\",\"path\":\"skill://diagnose\",\"selector\":null}","call_id":"call_HCYtxM7YM7vS5XzadyHRPDFA","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aa985988819ab311a2678929ef51","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping lifecycle sources\",\"path\":\"**/*.{ts,tsx,js,mjs,cjs,md,json}\",\"hidden\":true,\"gitignore\":true,\"limit\":500}","call_id":"call_Y1Hxk0fwkL33I2w9fhigUFBg","name":"glob"},{"id":"fc_0cc7a0c9a797f232016a58aa985994819a8bca666bf84a1e2e","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating refresh update paths\",\"pattern\":\"daemon refresh|\\\\brefresh\\\\b|\\\\bupdate\\\\b|restart|broker|brain\",\"path\":\"src;packages;test;tests;docs;CONTEXT.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_rgzsCOMS8u8nHxMIVq6rKAZE","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aa98599c819aa379f74978b45ace","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading domain context\",\"path\":\"CONTEXT.md\",\"selector\":null}","call_id":"call_3pZmXCuOduphweSgbvs6krJX","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aa9859a4819a9eb7710de6a124bc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading known hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":null}","call_id":"call_msUQmsgOctlX1F5N2SyLaHZ6","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aa9859d8819a88241bcdaed6db94","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rollout guidance\",\"path\":\"docs/DEBUG-ROLLOUT.md\",\"selector\":null}","call_id":"call_NErsR1cD7pN3wV7cXZBoomFS","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aa9859e0819a9d2fddd2371a28e4","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding architecture decisions\",\"path\":\"docs/adr/**/*.md\",\"hidden\":true,\"gitignore\":true,\"limit\":200}","call_id":"call_XnX2rK1jL04iKinxxGde6WU4","name":"glob"}]},"duration":7698.311300000176,"ttft":1448.7247999999672,"contextSnapshot":{"promptTokens":14541,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HCYtxM7YM7vS5XzadyHRPDFA|fc_0cc7a0c9a797f232016a58aa985970819ab9656292398d2639","toolName":"read","startedAt":"2026-07-16T09:55:36.409Z","args":{"path":"skill://diagnose"},"intent":"Reading diagnosis workflow"},"id":"95497be8","parentId":"181b3ff9","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Y1Hxk0fwkL33I2w9fhigUFBg|fc_0cc7a0c9a797f232016a58aa985988819ab311a2678929ef51","toolName":"glob","startedAt":"2026-07-16T09:55:36.410Z","args":{"path":"**/*.{ts,tsx,js,mjs,cjs,md,json}"},"intent":"Mapping lifecycle sources"},"id":"5f0d9447","parentId":"95497be8","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rgzsCOMS8u8nHxMIVq6rKAZE|fc_0cc7a0c9a797f232016a58aa985994819a8bca666bf84a1e2e","toolName":"grep","startedAt":"2026-07-16T09:55:36.410Z","args":{"path":"src;packages;test;tests;docs;CONTEXT.md"},"intent":"Locating refresh update paths"},"id":"8d0972f9","parentId":"5f0d9447","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3pZmXCuOduphweSgbvs6krJX|fc_0cc7a0c9a797f232016a58aa98599c819aa379f74978b45ace","toolName":"read","startedAt":"2026-07-16T09:55:36.410Z","args":{"path":"CONTEXT.md"},"intent":"Reading domain context"},"id":"d09f27b6","parentId":"8d0972f9","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_msUQmsgOctlX1F5N2SyLaHZ6|fc_0cc7a0c9a797f232016a58aa9859a4819a9eb7710de6a124bc","toolName":"read","startedAt":"2026-07-16T09:55:36.410Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading known hazards"},"id":"768c6832","parentId":"d09f27b6","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NErsR1cD7pN3wV7cXZBoomFS|fc_0cc7a0c9a797f232016a58aa9859d8819a88241bcdaed6db94","toolName":"read","startedAt":"2026-07-16T09:55:36.410Z","args":{"path":"docs/DEBUG-ROLLOUT.md"},"intent":"Reading rollout guidance"},"id":"4a5122cc","parentId":"768c6832","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XnX2rK1jL04iKinxxGde6WU4|fc_0cc7a0c9a797f232016a58aa9859e0819a9d2fddd2371a28e4","toolName":"glob","startedAt":"2026-07-16T09:55:36.410Z","args":{"path":"docs/adr/**/*.md"},"intent":"Finding architecture decisions"},"id":"af9c1e47","parentId":"4a5122cc","timestamp":"2026-07-16T09:55:36.410Z"}
{"type":"message","id":"d7e88a6b","parentId":"af9c1e47","timestamp":"2026-07-16T09:55:36.418Z","message":{"role":"toolResult","toolCallId":"call_XnX2rK1jL04iKinxxGde6WU4|fc_0cc7a0c9a797f232016a58aa9859e0819a9d2fddd2371a28e4","toolName":"glob","content":[{"type":"text","text":"# docs/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":"docs/adr","fileCount":36,"files":["docs/adr/0037-materialized-project-index.md","docs/adr/0036-private-fork-distribution.md","docs/adr/0005-totp-seeded-spake2-node-pairing.md","docs/adr/0031-server-side-screen-grid-render-repaint.md","docs/adr/0034-per-minter-op-id-namespacing.md","docs/adr/0033-wake-resume-via-reconcile-intent.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md","docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","docs/adr/0032-boundary-rotation-credential.md","docs/adr/0030-robust-wan-subnet-join.md","docs/adr/0029-adapter-update-arc-resolve-not-execute.md","docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md","docs/adr/0024-multi-platform-adapter-spt-packaging.md","docs/adr/0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md","docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md","docs/adr/0009-endpoint-access-control-model.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/adr/0016-platform-targeted-update-sets.md","docs/adr/0017-subnet-mesh-membership-seed-proof.md","docs/adr/0014-publish-target-repo-and-lapse-proof-canonical-url.md","docs/adr/0015-release-signing-two-key-trust-anchor.md","docs/adr/0013-psyche-reconciled-context-merge.md","docs/adr/0010-immutable-home-subnet-fork-not-rehome.md","docs/adr/0011-branchstore-git-kv-state-substrate.md","docs/adr/0006-multi-subnet-membership-model.md","docs/adr/0007-subnet-notification-primitive.md","docs/adr/0003-multi-instance-node-anchored-identity-model.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0002-bake-networking-into-core-collapse-spt-node.md","docs/adr/0001-clean-room-fork-from-claude-skill-owl.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784195736417}}
{"type":"message","id":"0ecac756","parentId":"d7e88a6b","timestamp":"2026-07-16T09:55:36.433Z","message":{"role":"toolResult","toolCallId":"call_Y1Hxk0fwkL33I2w9fhigUFBg|fc_0cc7a0c9a797f232016a58aa985988819ab311a2678929ef51","toolName":"glob","content":[{"type":"text","text":"CHANGELOG.md\nCONTEXT.md\nREADME.md\nCLAUDE.md\nV0.19.0-P6-SCREEN-GRID-DESIGN.md\nV0.17.0-ROBUST-JOIN-AND-PRESENCE-JIT.md\nV0.16.0-UPDATE-ARC-AND-CLI-JIT.md\nAGENTS.md\nV0.15.0-PLAN.md\nROADMAP.md\nF-019-TRANSLATION-BINARY-INSTALL-DIR-RESOLVE-DESIGN.md\nV0.14.1-HOME-SUBNET-PICKER-LAYER-DESIGN.md\nF-018-ADAPTER-ADD-NONDESTRUCTIVE-DESIGN.md\nV0.14.0-ENDPOINT-CREATION-FLOW-JIT.md\nW3-DESIGN-PROPOSAL.md\nW3-GATE-VERDICT.md\nW3-DESIGN-GATE.md\nV0.13.2-ADAPTER-PACKAGING-JIT.md\nV0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md\nV0.13.0-W4-PICKER-UX-PLAN.md\nV0.13.0-VIEWER-RING-ROLL-SNAP-JIT.md\nV0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md\nV0.13.0-SKIP-TO-LIVE-JIT.md\nV0.13.0-RC-VT-TRANSLATE-DESIGN.md\nV0.13.0-P2-SESSION-RESUME-DESIGN.md\nV0.13.0-P1C-CONTROLLER-WRITER-REORDER-PLAN.md\nV0.13.0-P1-RC-PASTE-DESIGN.md\nV0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md\nV0.13.0-P0-JIT-PLAN.md\nV0.13.0-DELIVERY-CONTROL-JIT.md\nV0.13.0-BUG2-RC-VT-TRANSLATE-PLAN.md\nV0.12.1-LIFECYCLE-JIT.md\nV074-PLAN.md\nM11-PLAN.md\nDAEMON-LIFECYCLE-COMPLETION-PLAN.md\nREQ-MSG-ENVELOPE-PLAN.md\nPRD.md\nM12-W4-RULING.md\nM12-W5-PLAN.md\nM12-W4-PLAN.md\nM12-W3-PLAN.md\nM12-W3-RULING.md\nM12-W2-RULING.md\nM12-W2.5-PLAN.md\nM12-W2.5-RULING.md\nM12-W2-PLAN.md\nM12-W1-PLAN.md\nM12-W1.5-PLAN.md\nM12-PLAN.md\nM12-ENDPOINT-RUN-PICKER.md\nM12-CANDIDATES.md\nDIGEST-MILESTONE-PLAN.md\nM9-WAVE3-JIT.md\nM9-WAVE2-ROLE-WRITERS.md\nM9-WAVE1-ADDRESSING-SURVEY.md\nM9-GATEWAY-WAN-PLAN.md\nM9-PLAN.md\nV042-PLAN.md\nPUMP-SEAM-PLAN.md\nRESTORATION-D7-PLAN.md\nRESTORATION-D6-PLAN.md\nRESTORATION-D5-PLAN.md\nRESTORATION-D4-PLAN.md\nRESTORATION-PLAN.md\nRESTORATION-D3-PLAN.md\nRESTORATION-D2-PLAN.md\nRESTORATION-D1-PLAN.md\nV032-PLAN.md\nV031-PLAN.md\n# docs-site/src/\nindex.md\nSUMMARY.md\n## cli/\nreference.md\n## self-update/\noverview.md\n## harness-contract/\nmanifest.md\nintegration-checklist.md\ninstall-on-demand.md\napi.md\noverview.md\necho-commune.md\npatterns.md\n## reference/\njson-shapes.md\ninstall.md\nschema.md\n## quickstart/\nmessaging.md\nadapter.md\n## instances/\noverview.md\n## messaging/\noverview.md\n## terminal/\noverview.md\n## lifecycle/\noverview.md\n## networking/\noverview.md\n## shells/\noverview.md\ngetting-started.md\n## concepts/\noverview.md\n# docs/\nSTORAGE.md\nPROJECT-INDEX-TRIAGE.md\nRELEASE-RUNBOOK.md\nTHE-FORKENING-TRIAGE.md\nUPDATE-WEDGE-2-ROUND3-CODEREAD.md\nUPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\nPUMP-W2-DESIGN.md\nPUMP-W2-GATE-HANDOFF.md\nPUMP-W2-BUILD-DELTA.md\nPUMP-W2-BUILD-GO.md\nKNOWN-HAZARDS.md\nMSG-IDENTITY-DISPATCH.md\nGATEWAY-LIVENESS-DISPATCH.md\nGATEWAY-RCA-STEP1.md\nF-035-RCA.md\nF-035-RELAY-RCA.md\nF-035-CONFLICT.md\nPUMP-TRUTH-TRIAGE.md\nPUMP-W2-DISPATCH-RULING.md\nMUSL-RELEASE-HANDOFF.md\nMUSL-W3-DISPATCH-RULING.md\nMUSL-W1-DISPATCH-RULING.md\nNEXT-MILESTONE-MUSL-TIER-TRIAGE.md\nW6-GATE-VERDICT.md\nW6-DISPATCH-RULING.md\nW5-WAVE-GATE-REPORT.md\nW5-GATE-VERDICT.md\nW5-GATE-HOLD.md\nW5-DISPATCH-RULING.md\nW4-WAVE-GATE-REPORT.md\nW4-GATE-VERDICT.md\nW4-DISPATCH-RULING.md\nW3-WAVE-GATE-REPORT.md\nW3-GATE-VERDICT.md\nW3-SLICE1-REPORT.md\nW2-GATE-VERDICT.md\nW3-DIGEST-GENERATION-UNION-RULING.md\nW3-ENDPOINT-SURVIVAL-DESIGN.md\nW2-DESIGN-RULING.md\nW2-BUILT-REPORT.md\nW2-DESIGN-PROPOSAL.md\nNEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\nINJECT-SETTLE-REARM-GATE-VERDICT.md\nNEXT-MILESTONE-WORKERTRUTH-TRIAGE.md\nFLAKE-LEDGER.md\nTWO-HOST-RUNBOOK.md\nNEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\nMANIFEST.md\nFAULT-MATRIX.md\nCONTEXT-MEMORY.md\nF-030-PSYCHE-EPHEMERAL-DESIGN.md\nNEXT-MILESTONE-PICKER-POLISH-TRIAGE.md\nF-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\nNEXT-MILESTONE-RUNTRUTH-TRIAGE.md\nNEXT-MILESTONE-PICKER-TRIAGE.md\nNEXT-MILESTONE-BUG-TRIAGE.md\nDOCS-STRATEGY.md\nDEFERRED.md\nBROKER-BRAIN-SPLIT-RESTORATION.md\nDORMANCY-BUDGET.md\nDEBUG-CONVERGE-PLAN.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\n## design/\nsubnet-presence-display.md\nrobust-wan-subnet-join.md\nv0.16.0-update-arc-and-cli.md\n## spikes/\nSPIKE-06-idempotent-boundary.md\nSPIKE-07-branchstore.md\nSPIKE-04-forkpty-parity.md\nSPIKE-05-restart-stress.md\nSPIKE-03-quic-survival.md\n# crates/spt-runtime/\nmanifest.schema.json\n# releases-repo/\nSYNC.md\nREADME.md\n\n[200 results limit reached. Use limit=400 for more]"}],"details":{"scopePath":".","fileCount":200,"files":["CHANGELOG.md","docs-site/src/cli/reference.md","docs-site/src/self-update/overview.md","docs-site/src/harness-contract/manifest.md","docs-site/src/harness-contract/integration-checklist.md","CONTEXT.md","docs/STORAGE.md","docs-site/src/reference/json-shapes.md","docs-site/src/reference/install.md","docs-site/src/quickstart/messaging.md","docs-site/src/harness-contract/install-on-demand.md","docs-site/src/index.md","README.md","docs/adr/0037-materialized-project-index.md","docs/PROJECT-INDEX-TRIAGE.md","docs-site/src/harness-contract/api.md","docs/RELEASE-RUNBOOK.md","docs-site/src/quickstart/adapter.md","docs-site/src/harness-contract/overview.md","crates/spt-runtime/manifest.schema.json","docs-site/src/reference/schema.md","docs-site/src/SUMMARY.md","docs/THE-FORKENING-TRIAGE.md","docs/adr/0036-private-fork-distribution.md","docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/PUMP-W2-DESIGN.md","docs/PUMP-W2-GATE-HANDOFF.md","docs/PUMP-W2-BUILD-DELTA.md","docs/PUMP-W2-BUILD-GO.md","docs/KNOWN-HAZARDS.md","docs/MSG-IDENTITY-DISPATCH.md","docs/GATEWAY-LIVENESS-DISPATCH.md","docs/GATEWAY-RCA-STEP1.md","docs/F-035-RCA.md","docs/F-035-RELAY-RCA.md","docs-site/src/instances/overview.md","docs/F-035-CONFLICT.md","docs/PUMP-TRUTH-TRIAGE.md","docs/PUMP-W2-DISPATCH-RULING.md","docs/MUSL-RELEASE-HANDOFF.md","docs/MUSL-W3-DISPATCH-RULING.md","docs/MUSL-W1-DISPATCH-RULING.md","docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md","docs/W6-GATE-VERDICT.md","docs/W6-DISPATCH-RULING.md","docs/W5-WAVE-GATE-REPORT.md","docs/W5-GATE-VERDICT.md","docs/W5-GATE-HOLD.md","docs/W5-DISPATCH-RULING.md","docs/W4-WAVE-GATE-REPORT.md","docs/W4-GATE-VERDICT.md","docs/W4-DISPATCH-RULING.md","docs/W3-WAVE-GATE-REPORT.md","docs/W3-GATE-VERDICT.md","docs/W3-SLICE1-REPORT.md","docs/W2-GATE-VERDICT.md","docs/W3-DIGEST-GENERATION-UNION-RULING.md","docs/W3-ENDPOINT-SURVIVAL-DESIGN.md","docs/W2-DESIGN-RULING.md","docs/W2-BUILT-REPORT.md","docs/W2-DESIGN-PROPOSAL.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","docs-site/src/messaging/overview.md","docs-site/src/harness-contract/echo-commune.md","docs/adr/0005-totp-seeded-spake2-node-pairing.md","docs/NEXT-MILESTONE-WORKERTRUTH-TRIAGE.md","docs-site/src/terminal/overview.md","docs/FLAKE-LEDGER.md","docs-site/src/harness-contract/patterns.md","docs/adr/0031-server-side-screen-grid-render-repaint.md","docs/adr/0034-per-minter-op-id-namespacing.md","docs/TWO-HOST-RUNBOOK.md","docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","docs/MANIFEST.md","docs/FAULT-MATRIX.md","CLAUDE.md","docs/adr/0033-wake-resume-via-reconcile-intent.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md","docs/CONTEXT-MEMORY.md","docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","docs-site/src/lifecycle/overview.md","docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md","docs/adr/0032-boundary-rotation-credential.md","docs/NEXT-MILESTONE-PICKER-TRIAGE.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","V0.19.0-P6-SCREEN-GRID-DESIGN.md","docs-site/src/networking/overview.md","V0.17.0-ROBUST-JOIN-AND-PRESENCE-JIT.md","docs/design/subnet-presence-display.md","docs/adr/0030-robust-wan-subnet-join.md","docs/design/robust-wan-subnet-join.md","docs/design/v0.16.0-update-arc-and-cli.md","V0.16.0-UPDATE-ARC-AND-CLI-JIT.md","docs/adr/0029-adapter-update-arc-resolve-not-execute.md","AGENTS.md","V0.15.0-PLAN.md","docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","ROADMAP.md","F-019-TRANSLATION-BINARY-INSTALL-DIR-RESOLVE-DESIGN.md","V0.14.1-HOME-SUBNET-PICKER-LAYER-DESIGN.md","F-018-ADAPTER-ADD-NONDESTRUCTIVE-DESIGN.md","docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md","V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md","docs/adr/0024-multi-platform-adapter-spt-packaging.md","docs/DOCS-STRATEGY.md","W3-DESIGN-PROPOSAL.md","W3-GATE-VERDICT.md","W3-DESIGN-GATE.md","V0.13.2-ADAPTER-PACKAGING-JIT.md","docs/DEFERRED.md","docs/adr/0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md","V0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md","V0.13.0-W4-PICKER-UX-PLAN.md","V0.13.0-VIEWER-RING-ROLL-SNAP-JIT.md","V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md","V0.13.0-SKIP-TO-LIVE-JIT.md","V0.13.0-RC-VT-TRANSLATE-DESIGN.md","V0.13.0-P2-SESSION-RESUME-DESIGN.md","V0.13.0-P1C-CONTROLLER-WRITER-REORDER-PLAN.md","V0.13.0-P1-RC-PASTE-DESIGN.md","V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md","V0.13.0-P0-JIT-PLAN.md","V0.13.0-DELIVERY-CONTROL-JIT.md","V0.13.0-BUG2-RC-VT-TRANSLATE-PLAN.md","V0.12.1-LIFECYCLE-JIT.md","docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","V074-PLAN.md","docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","docs-site/src/shells/overview.md","docs-site/src/shells/getting-started.md","M11-PLAN.md","DAEMON-LIFECYCLE-COMPLETION-PLAN.md","docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md","docs/adr/0009-endpoint-access-control-model.md","docs-site/src/concepts/overview.md","REQ-MSG-ENVELOPE-PLAN.md","PRD.md","M12-W4-RULING.md","M12-W5-PLAN.md","M12-W4-PLAN.md","M12-W3-PLAN.md","M12-W3-RULING.md","M12-W2-RULING.md","M12-W2.5-PLAN.md","M12-W2.5-RULING.md","M12-W2-PLAN.md","M12-W1-PLAN.md","M12-W1.5-PLAN.md","M12-PLAN.md","M12-ENDPOINT-RUN-PICKER.md","M12-CANDIDATES.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md","DIGEST-MILESTONE-PLAN.md","M9-WAVE3-JIT.md","M9-WAVE2-ROLE-WRITERS.md","M9-WAVE1-ADDRESSING-SURVEY.md","M9-GATEWAY-WAN-PLAN.md","M9-PLAN.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","V042-PLAN.md","PUMP-SEAM-PLAN.md","RESTORATION-D7-PLAN.md","RESTORATION-D6-PLAN.md","RESTORATION-D5-PLAN.md","RESTORATION-D4-PLAN.md","RESTORATION-PLAN.md","RESTORATION-D3-PLAN.md","RESTORATION-D2-PLAN.md","RESTORATION-D1-PLAN.md","docs/BROKER-BRAIN-SPLIT-RESTORATION.md","V032-PLAN.md","V031-PLAN.md","releases-repo/SYNC.md","releases-repo/README.md","docs/spikes/SPIKE-06-idempotent-boundary.md","docs/spikes/SPIKE-07-branchstore.md","docs/spikes/SPIKE-04-forkpty-parity.md","docs/spikes/SPIKE-05-restart-stress.md","docs/spikes/SPIKE-03-quic-survival.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/DORMANCY-BUDGET.md","docs/DEBUG-CONVERGE-PLAN.md"],"truncated":true,"resultLimitReached":200,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","meta":{"limits":{"resultLimit":{"reached":200,"suggestion":400}}}},"isError":false,"timestamp":1784195736433}}
{"type":"message","id":"68b5b435","parentId":"0ecac756","timestamp":"2026-07-16T09:55:36.436Z","message":{"role":"toolResult","toolCallId":"call_HCYtxM7YM7vS5XzadyHRPDFA|fc_0cc7a0c9a797f232016a58aa985970819ab9656292398d2639","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":1784195736436}}
{"type":"message","id":"05e3abc5","parentId":"68b5b435","timestamp":"2026-07-16T09:55:36.438Z","message":{"role":"toolResult","toolCallId":"call_NErsR1cD7pN3wV7cXZBoomFS|fc_0cc7a0c9a797f232016a58aa9859d8819a88241bcdaed6db94","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":1784195736438}}
{"type":"message","id":"f7199b28","parentId":"05e3abc5","timestamp":"2026-07-16T09:55:36.478Z","message":{"role":"toolResult","toolCallId":"call_3pZmXCuOduphweSgbvs6krJX|fc_0cc7a0c9a797f232016a58aa98599c819aa379f74978b45ace","toolName":"read","content":[{"type":"text","text":"# spt-core\n\n**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\n\n**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.\n\nHarness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.\n\nSuccessor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.\n\n## Language\n\n**spt-core**:\nThe system. Canonical name. The Rust workspace and the umbrella project.\n\n**spt.exe / spt** (canonical binary):\nThe reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.\n\n**library workspace**:\nThe set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.\n\n**spt plugin** (separate downstream project — NOT an spt-core deliverable):\nA rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PR…\n\n**Pi** (disambiguation — two meanings, never conflate):\n(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.\n_Avoid_: bare \"Pi node\" when the harness is meant.\n\n**spt-daemon** (per-machine supervisor):\nThe single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.\n\nInternally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\n- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\n- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\n\nLogical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\n\n**in-session relay**:\nA thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{…\n\n### Deliverable shape\n\nspt-core ships **both** a library workspace and a canonical binary:\n\n- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\n- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).\n\nBoth surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).\n\n## Runtime model\n\nspt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\n\n**AgentRuntime** (Rust trait, implementation detail):\nThe internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.\n\n**harness contract** (umbrella term):\nThe full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.\n\n**runtime manifest** (outbound half of the harness contract):\nA declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.\n\nExample shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.\n\n**subcommand surface** (inbound half of the harness contract):\nThe stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.\n\n**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.\n\nTogether: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.\n\n**adapter manifest header** (`adapter_name` + version compat):\nEvery manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_ver…\n\n<!-- [doc->REQ-MANIFEST-2] -->\n**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):\nA named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local …\n_Avoid_: \"manifest fork\", \"child adapter\", per-profile versioning.\n\n**adapter strings** (ratified 2026-06-11, Gateway grill):\n<!-- [doc->REQ-MANIFEST-3] -->\nA `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** p…\n<!-- [doc->REQ-MANIFEST-5] -->\n**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = \"skill.md\" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversa…\n_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); \"adapter KV store\" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).\n\n**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):\n`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packe…\n_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).\n\n**keyword hints** (ratified 2026-06-12 — core milestone A):\n<!-- [doc->REQ-MANIFEST-4] -->\nOnce-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hin…\n_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\n\n**adapter update declaration** (manifest field):\n<!-- [doc->REQ-UPD-9] -->\nEach adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = \"user/repo\"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed …\n\n**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):\n<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\nA `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS…\n<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\nAn optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\n\n**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):\nAn optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI re…\n\n**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.\n_Avoid_: calling the Psyche loop or an on-demand extractor a \"resident\" binary; \"restart the endpoint\" for what is a per-binary cycle.\n\n**session-invocation declaration** (manifest field, noted for spt-plugin parity):\nHow the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.\n\n### Manifest seams (outbound contract, detailed)\n\nGoverning principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).\n\n**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first …\n- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\n\n**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.\n\n**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.\n\n**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.\n\n**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:\n- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.\n- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.\n- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.\n- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence mode…\n- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = \"read\"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` direc…\n- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to lo…\n\n**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.\n\n**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:\n- PTY injection (with or without key/submit sequences) — spt-hosted topology;\n- adapter hooks calling spt-core poll commands;\n- an in-adapter-session child relay (à la CC's Monitor tool);\n- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).\nNote: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.\n\n**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.\n\n**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single \"type\". The flag on each axis defaults to the unrestricted value:\n- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.\n- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *\"Native\"* = the `[message-idle-translation-binary]` PTY channel.\n- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay \"window opens with no live *listener*\" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-co…\nWindow restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).\n\n**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).\n\n**resume-session seam** — two distinct forms:\n- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune…\n- **continue-existing:** resume an existing harness session under the adapter (its native resume).\n\n**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.\n\n**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.\n\nThere is no separate \"model/billing\" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:\n\n- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\n- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-h…\n- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\n- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\n- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapte…\n- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.\n- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or …\n\n### Inbound `api` surface (detailed)\n\nAll commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.\n\n<!-- [doc->REQ-API-4] -->\n**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.\n\n- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\n- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.\n- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).\n- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.\n- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\n- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).\n- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-t…\n- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).\n\n**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own run…\n_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).\n<!-- [doc->REQ-ENDPOINT-PURGE] -->\n\n- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\n- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\n- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\n\n**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.\n\n### Startup flows (the two topologies)\n\n**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\n\n**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:\n1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persis…\n2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.\n3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.\n   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.\n   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).\n\n**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:\n1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\n2. **candidate adapters** = registered `kind=\"harness\"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->\n3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->\n4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.\n\n**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.\n\n**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\n1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.\n2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.\n3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.\n4. Psyche is daemon-spawned, same as above.\n\nSo the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.\n\n**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).\n\n### Endpoint types\n\nEach perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:\n\n**ReadyAgent**:\nMinimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\n\n**LiveAgent**:\nA Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's \"live agent\".\n\n**Psyche**:\nThe Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any …\n\n*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type=\"reply\">`/`<EVENT type=\"notify\">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).\n\n*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state**…\n\n**Summarizer**:\nThe ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.\n_Avoid_: conflating with the Psyche; \"echo-commune model\".\n\n**Worker**:\nA subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's \"working perch\" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.\n\n**SptNode**:\nA machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.\n\n<!-- [doc->REQ-EP-6] -->\n**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):\nA **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).\n\n<!-- [doc->REQ-MSG-5] -->\nA message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.\n\n<!-- [doc->REQ-MSG-6] -->\n_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state=\"gateway\"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`e…\n\nA Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[AlwaysOnEndpoint]] resident binary) holds the device-contact surface and fires the standard **wake resolution** (\"owner suspended → revive the owner\"). No Gateway-manifest watcher, no autostart flag, no new mechan…\n_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\n\n**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\nA *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent \"just knows how to reach the user\" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.\n\nThree interaction styles:\n- **dispatch** — fire-and-forget: \"reach the user with this payload\"; channel delivers via the best available Shell.\n- **bind** — sustained drive: \"give me a Shell of capability X\"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).\n- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.\n\nPresence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.\n\n**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):\nA **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), an…\n_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); \"service\" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).\n\n**instance state (active / dormant / suspended / offline)**:\nThe four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):\n- **active** — the **actively-driven** instance: the one a bare-`id` message resolves to. (Driving `ling@laptop` makes `ling@desktop` **dormant**.)\n- **dormant** — **warm** (still running / in-memory) but **not the active target** — a sibling took attention (`AttentionShift`) or the driver detached (`Detach`). Genuinely *available* (a valid routing fallback) — the picker shows it online. Decays to *suspended* via an auto-suspend timer.\n- **suspended** — **cold**: the session is closed, **resumed-on-wake**, but **its node is up** (the daemon still gossips the row); still **addressable** (a `wake` must route).\n- **offline** — the endpoint's **node is down** / unroutable. **Never self-gossiped** (a down node cannot gossip) — a remote viewer infers it when the node stops gossiping (epoch-lease eviction); the resolver skips it.\n\nThe active/dormant discriminator is **running-ness + attention** (warm and which instance holds the drive); the suspended/offline discriminator is **node up vs node down**. _Avoid_: advertising a **not-running** (cold, no live session) perch as `dormant` — `dormant` requires the session to be warm/running; a cold perch on a **live** node is **suspended** (node up, endpoint cold), and a live node **never** self-gossips `offline`. (Bound-gated subtlety: an **unbound** perch reads `is_perch_alive==false` yet has a live broker session — it is still *warm*, so it is `active`/`dormant`, not suspended.) _Also avoid_: treating **corrupt** (a present-but-destroyed perch record) as a fifth instance state — it is a **record condition**, not a liveness state. A corrupt …\n\n**effective instance state**: The instance state a reader acts on is always **derived** — liveness discriminates warm/cold (with the unbound subtlety above), and the stored rest intent (`dormant` vs `suspended`) refines only *within* warm. No single stored field is authoritative for \"resting-cold\"; any reader that trusts a stored rest field against observed liveness (or vice versa) is wrong by construction. _Avoid_: treating the rest-intent record as cold-truth, or defaulting an absent intent to *active*.\n\nThe endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.\n\n### Agent endpoints vs Shells\n\nEndpoint types split into two families:\n\n**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.\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-300 of 883. Use :301 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"# spt-core\r\n\r\n**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\r\n\r\n**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.\r\n\r\nHarness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.\r\n\r\nSuccessor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.\r\n\r\n## Language\r\n\r\n**spt-core**:\r\nThe system. Canonical name. The Rust workspace and the umbrella project.\r\n\r\n**spt.exe / spt** (canonical binary):\r\nThe reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.\r\n\r\n**library workspace**:\r\nThe set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.\r\n\r\n**spt plugin** (separate downstream project — NOT an spt-core deliverable):\r\nA rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PR…\n\r\n**Pi** (disambiguation — two meanings, never conflate):\r\n(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.\r\n_Avoid_: bare \"Pi node\" when the harness is meant.\r\n\r\n**spt-daemon** (per-machine supervisor):\r\nThe single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.\r\n\r\nInternally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\r\n- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\r\n- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\r\n\r\nLogical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\r\n\r\n**in-session relay**:\r\nA thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{…\n\r\n### Deliverable shape\r\n\r\nspt-core ships **both** a library workspace and a canonical binary:\r\n\r\n- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\r\n- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).\r\n\r\nBoth surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).\r\n\r\n## Runtime model\r\n\r\nspt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\r\n\r\n**AgentRuntime** (Rust trait, implementation detail):\r\nThe internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.\r\n\r\n**harness contract** (umbrella term):\r\nThe full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.\r\n\r\n**runtime manifest** (outbound half of the harness contract):\r\nA declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.\r\n\r\nExample shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.\r\n\r\n**subcommand surface** (inbound half of the harness contract):\r\nThe stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.\r\n\r\n**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.\r\n\r\nTogether: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.\r\n\r\n**adapter manifest header** (`adapter_name` + version compat):\r\nEvery manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_ver…\n\r\n<!-- [doc->REQ-MANIFEST-2] -->\r\n**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):\r\nA named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local …\n_Avoid_: \"manifest fork\", \"child adapter\", per-profile versioning.\r\n\r\n**adapter strings** (ratified 2026-06-11, Gateway grill):\r\n<!-- [doc->REQ-MANIFEST-3] -->\r\nA `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** p…\n<!-- [doc->REQ-MANIFEST-5] -->\r\n**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = \"skill.md\" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversa…\n_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); \"adapter KV store\" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).\r\n\r\n**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):\r\n`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packe…\n_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).\r\n\r\n**keyword hints** (ratified 2026-06-12 — core milestone A):\r\n<!-- [doc->REQ-MANIFEST-4] -->\r\nOnce-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hin…\n_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\r\n\r\n**adapter update declaration** (manifest field):\r\n<!-- [doc->REQ-UPD-9] -->\r\nEach adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = \"user/repo\"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed …\n\r\n**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):\r\n<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\r\nA `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS…\n<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\r\nAn optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\r\n\r\n**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):\r\nAn optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI re…\n\r\n**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.\r\n_Avoid_: calling the Psyche loop or an on-demand extractor a \"resident\" binary; \"restart the endpoint\" for what is a per-binary cycle.\r\n\r\n**session-invocation declaration** (manifest field, noted for spt-plugin parity):\r\nHow the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.\r\n\r\n### Manifest seams (outbound contract, detailed)\r\n\r\nGoverning principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).\r\n\r\n**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first …\n- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\r\n\r\n**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.\r\n\r\n**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.\r\n\r\n**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.\r\n\r\n**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:\r\n- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.\r\n- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.\r\n- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.\r\n- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence mode…\n- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = \"read\"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` direc…\n- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to lo…\n\r\n**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.\r\n\r\n**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:\r\n- PTY injection (with or without key/submit sequences) — spt-hosted topology;\r\n- adapter hooks calling spt-core poll commands;\r\n- an in-adapter-session child relay (à la CC's Monitor tool);\r\n- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).\r\nNote: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.\r\n\r\n**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.\r\n\r\n**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single \"type\". The flag on each axis defaults to the unrestricted value:\r\n- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.\r\n- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *\"Native\"* = the `[message-idle-translation-binary]` PTY channel.\r\n- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay \"window opens with no live *listener*\" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-co…\nWindow restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).\r\n\r\n**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).\r\n\r\n**resume-session seam** — two distinct forms:\r\n- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune…\n- **continue-existing:** resume an existing harness session under the adapter (its native resume).\r\n\r\n**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.\r\n\r\n**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.\r\n\r\nThere is no separate \"model/billing\" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:\r\n\r\n- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\r\n- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-h…\n- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\r\n- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\r\n- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapte…\n- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.\r\n- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or …\n\r\n### Inbound `api` surface (detailed)\r\n\r\nAll commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.\r\n\r\n<!-- [doc->REQ-API-4] -->\r\n**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.\r\n\r\n- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\r\n- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.\r\n- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).\r\n- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.\r\n- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\r\n- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).\r\n- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-t…\n- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).\r\n\r\n**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own run…\n_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).\r\n<!-- [doc->REQ-ENDPOINT-PURGE] -->\r\n\r\n- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\r\n- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\r\n- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\r\n\r\n**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.\r\n\r\n### Startup flows (the two topologies)\r\n\r\n**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\r\n\r\n**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:\r\n1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persis…\n2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.\r\n3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.\r\n   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.\r\n   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).\r\n\r\n**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:\r\n1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\r\n2. **candidate adapters** = registered `kind=\"harness\"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->\r\n3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->\r\n4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.\r\n\r\n**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.\r\n\r\n**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\r\n1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.\r\n2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.\r\n3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.\r\n4. Psyche is daemon-spawned, same as above.\r\n\r\nSo the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.\r\n\r\n**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).\r\n\r\n### Endpoint types\r\n\r\nEach perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:\r\n\r\n**ReadyAgent**:\r\nMinimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\r\n\r\n**LiveAgent**:\r\nA Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's \"live agent\".\r\n\r\n**Psyche**:\r\nThe Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any …\n\r\n*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type=\"reply\">`/`<EVENT type=\"notify\">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).\r\n\r\n*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state**…\n\r\n**Summarizer**:\r\nThe ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.\r\n_Avoid_: conflating with the Psyche; \"echo-commune model\".\r\n\r\n**Worker**:\r\nA subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's \"working perch\" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.\r\n\r\n**SptNode**:\r\nA machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.\r\n\r\n<!-- [doc->REQ-EP-6] -->\r\n**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):\r\nA **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).\r\n\r\n<!-- [doc->REQ-MSG-5] -->\r\nA message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.\r\n\r\n<!-- [doc->REQ-MSG-6] -->\r\n_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state=\"gateway\"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`e…\n\r\nA Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[AlwaysOnEndpoint]] resident binary) holds the device-contact surface and fires the standard **wake resolution** (\"owner suspended → revive the owner\"). No Gateway-manifest watcher, no autostart flag, no new mechan…\n_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\r\n\r\n**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\r\nA *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent \"just knows how to reach the user\" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.\r\n\r\nThree interaction styles:\r\n- **dispatch** — fire-and-forget: \"reach the user with this payload\"; channel delivers via the best available Shell.\r\n- **bind** — sustained drive: \"give me a Shell of capability X\"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).\r\n- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.\r\n\r\nPresence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.\r\n\r\n**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):\r\nA **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), an…\n_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); \"service\" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).\r\n\r\n**instance state (active / dormant / suspended / offline)**:\r\nThe four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):\r\n- **active** — the **actively-driven** instance: the one a bare-`id` message resolves to. (Driving `ling@laptop` makes `ling@desktop` **dormant**.)\r\n- **dormant** — **warm** (still running / in-memory) but **not the active target** — a sibling took attention (`AttentionShift`) or the driver detached (`Detach`). Genuinely *available* (a valid routing fallback) — the picker shows it online. Decays to *suspended* via an auto-suspend timer.\r\n- **suspended** — **cold**: the session is closed, **resumed-on-wake**, but **its node is up** (the daemon still gossips the row); still **addressable** (a `wake` must route).\r\n- **offline** — the endpoint's **node is down** / unroutable. **Never self-gossiped** (a down node cannot gossip) — a remote viewer infers it when the node stops gossiping (epoch-lease eviction); the resolver skips it.\r\n\r\nThe active/dormant discriminator is **running-ness + attention** (warm and which instance holds the drive); the suspended/offline discriminator is **node up vs node down**. _Avoid_: advertising a **not-running** (cold, no live session) perch as `dormant` — `dormant` requires the session to be warm/running; a cold perch on a **live** node is **suspended** (node up, endpoint cold), and a live node **never** self-gossips `offline`. (Bound-gated subtlety: an **unbound** perch reads `is_perch_alive==false` yet has a live broker session — it is still *warm*, so it is `active`/`dormant`, not suspended.) _Also avoid_: treating **corrupt** (a present-but-destroyed perch record) as a fifth instance state — it is a **record condition**, not a liveness state. A corrupt …\n\r\n**effective instance state**: The instance state a reader acts on is always **derived** — liveness discriminates warm/cold (with the unbound subtlety above), and the stored rest intent (`dormant` vs `suspended`) refines only *within* warm. No single stored field is authoritative for \"resting-cold\"; any reader that trusts a stored rest field against observed liveness (or vice versa) is wrong by construction. _Avoid_: treating the rest-intent record as cold-truth, or defaulting an absent intent to *active*.\r\n\r\nThe endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.\r\n\r\n### Agent endpoints vs Shells\r\n\r\nEndpoint types split into two families:\r\n\r\n**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":883,"totalBytes":72413,"outputLines":300,"outputBytes":72413,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# spt-core\r\n\r\n**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\r\n\r\n**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.\r\n\r\nHarness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.\r\n\r\nSuccessor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.\r\n\r\n## Language\r\n\r\n**spt-core**:\r\nThe system. Canonical name. The Rust workspace and the umbrella project.\r\n\r\n**spt.exe / spt** (canonical binary):\r\nThe reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.\r\n\r\n**library workspace**:\r\nThe set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.\r\n\r\n**spt plugin** (separate downstream project — NOT an spt-core deliverable):\r\nA rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PR…\n\r\n**Pi** (disambiguation — two meanings, never conflate):\r\n(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.\r\n_Avoid_: bare \"Pi node\" when the harness is meant.\r\n\r\n**spt-daemon** (per-machine supervisor):\r\nThe single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.\r\n\r\nInternally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\r\n- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\r\n- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\r\n\r\nLogical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\r\n\r\n**in-session relay**:\r\nA thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{…\n\r\n### Deliverable shape\r\n\r\nspt-core ships **both** a library workspace and a canonical binary:\r\n\r\n- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\r\n- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).\r\n\r\nBoth surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).\r\n\r\n## Runtime model\r\n\r\nspt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\r\n\r\n**AgentRuntime** (Rust trait, implementation detail):\r\nThe internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.\r\n\r\n**harness contract** (umbrella term):\r\nThe full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.\r\n\r\n**runtime manifest** (outbound half of the harness contract):\r\nA declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.\r\n\r\nExample shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.\r\n\r\n**subcommand surface** (inbound half of the harness contract):\r\nThe stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.\r\n\r\n**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.\r\n\r\nTogether: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.\r\n\r\n**adapter manifest header** (`adapter_name` + version compat):\r\nEvery manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_ver…\n\r\n<!-- [doc->REQ-MANIFEST-2] -->\r\n**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):\r\nA named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local …\n_Avoid_: \"manifest fork\", \"child adapter\", per-profile versioning.\r\n\r\n**adapter strings** (ratified 2026-06-11, Gateway grill):\r\n<!-- [doc->REQ-MANIFEST-3] -->\r\nA `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** p…\n<!-- [doc->REQ-MANIFEST-5] -->\r\n**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = \"skill.md\" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversa…\n_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); \"adapter KV store\" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).\r\n\r\n**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):\r\n`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packe…\n_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).\r\n\r\n**keyword hints** (ratified 2026-06-12 — core milestone A):\r\n<!-- [doc->REQ-MANIFEST-4] -->\r\nOnce-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hin…\n_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\r\n\r\n**adapter update declaration** (manifest field):\r\n<!-- [doc->REQ-UPD-9] -->\r\nEach adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = \"user/repo\"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed …\n\r\n**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):\r\n<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\r\nA `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS…\n<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\r\nAn optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\r\n\r\n**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):\r\nAn optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI re…\n\r\n**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.\r\n_Avoid_: calling the Psyche loop or an on-demand extractor a \"resident\" binary; \"restart the endpoint\" for what is a per-binary cycle.\r\n\r\n**session-invocation declaration** (manifest field, noted for spt-plugin parity):\r\nHow the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.\r\n\r\n### Manifest seams (outbound contract, detailed)\r\n\r\nGoverning principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).\r\n\r\n**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first …\n- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\r\n\r\n**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.\r\n\r\n**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.\r\n\r\n**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.\r\n\r\n**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:\r\n- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.\r\n- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.\r\n- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.\r\n- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence mode…\n- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = \"read\"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` direc…\n- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to lo…\n\r\n**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.\r\n\r\n**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:\r\n- PTY injection (with or without key/submit sequences) — spt-hosted topology;\r\n- adapter hooks calling spt-core poll commands;\r\n- an in-adapter-session child relay (à la CC's Monitor tool);\r\n- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).\r\nNote: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.\r\n\r\n**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.\r\n\r\n**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single \"type\". The flag on each axis defaults to the unrestricted value:\r\n- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.\r\n- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *\"Native\"* = the `[message-idle-translation-binary]` PTY channel.\r\n- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay \"window opens with no live *listener*\" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-co…\nWindow restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).\r\n\r\n**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).\r\n\r\n**resume-session seam** — two distinct forms:\r\n- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune…\n- **continue-existing:** resume an existing harness session under the adapter (its native resume).\r\n\r\n**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.\r\n\r\n**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.\r\n\r\nThere is no separate \"model/billing\" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:\r\n\r\n- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\r\n- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-h…\n- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\r\n- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\r\n- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapte…\n- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.\r\n- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or …\n\r\n### Inbound `api` surface (detailed)\r\n\r\nAll commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.\r\n\r\n<!-- [doc->REQ-API-4] -->\r\n**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.\r\n\r\n- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\r\n- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.\r\n- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).\r\n- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.\r\n- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\r\n- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).\r\n- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-t…\n- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).\r\n\r\n**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own run…\n_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).\r\n<!-- [doc->REQ-ENDPOINT-PURGE] -->\r\n\r\n- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\r\n- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\r\n- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\r\n\r\n**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.\r\n\r\n### Startup flows (the two topologies)\r\n\r\n**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\r\n\r\n**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:\r\n1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persis…\n2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.\r\n3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.\r\n   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.\r\n   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).\r\n\r\n**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:\r\n1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\r\n2. **candidate adapters** = registered `kind=\"harness\"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->\r\n3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->\r\n4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.\r\n\r\n**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.\r\n\r\n**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\r\n1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.\r\n2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.\r\n3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.\r\n4. Psyche is daemon-spawned, same as above.\r\n\r\nSo the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.\r\n\r\n**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).\r\n\r\n### Endpoint types\r\n\r\nEach perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:\r\n\r\n**ReadyAgent**:\r\nMinimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\r\n\r\n**LiveAgent**:\r\nA Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's \"live agent\".\r\n\r\n**Psyche**:\r\nThe Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any …\n\r\n*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type=\"reply\">`/`<EVENT type=\"notify\">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).\r\n\r\n*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state**…\n\r\n**Summarizer**:\r\nThe ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.\r\n_Avoid_: conflating with the Psyche; \"echo-commune model\".\r\n\r\n**Worker**:\r\nA subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's \"working perch\" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.\r\n\r\n**SptNode**:\r\nA machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.\r\n\r\n<!-- [doc->REQ-EP-6] -->\r\n**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):\r\nA **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).\r\n\r\n<!-- [doc->REQ-MSG-5] -->\r\nA message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.\r\n\r\n<!-- [doc->REQ-MSG-6] -->\r\n_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state=\"gateway\"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`e…\n\r\nA Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[AlwaysOnEndpoint]] resident binary) holds the device-contact surface and fires the standard **wake resolution** (\"owner suspended → revive the owner\"). No Gateway-manifest watcher, no autostart flag, no new mechan…\n_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\r\n\r\n**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\r\nA *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent \"just knows how to reach the user\" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.\r\n\r\nThree interaction styles:\r\n- **dispatch** — fire-and-forget: \"reach the user with this payload\"; channel delivers via the best available Shell.\r\n- **bind** — sustained drive: \"give me a Shell of capability X\"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).\r\n- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.\r\n\r\nPresence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.\r\n\r\n**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):\r\nA **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), an…\n_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); \"service\" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).\r\n\r\n**instance state (active / dormant / suspended / offline)**:\r\nThe four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):\r\n- **active** — the **actively-driven** instance: the one a bare-`id` message resolves to. (Driving `ling@laptop` makes `ling@desktop` **dormant**.)\r\n- **dormant** — **warm** (still running / in-memory) but **not the active target** — a sibling took attention (`AttentionShift`) or the driver detached (`Detach`). Genuinely *available* (a valid routing fallback) — the picker shows it online. Decays to *suspended* via an auto-suspend timer.\r\n- **suspended** — **cold**: the session is closed, **resumed-on-wake**, but **its node is up** (the daemon still gossips the row); still **addressable** (a `wake` must route).\r\n- **offline** — the endpoint's **node is down** / unroutable. **Never self-gossiped** (a down node cannot gossip) — a remote viewer infers it when the node stops gossiping (epoch-lease eviction); the resolver skips it.\r\n\r\nThe active/dormant discriminator is **running-ness + attention** (warm and which instance holds the drive); the suspended/offline discriminator is **node up vs node down**. _Avoid_: advertising a **not-running** (cold, no live session) perch as `dormant` — `dormant` requires the session to be warm/running; a cold perch on a **live** node is **suspended** (node up, endpoint cold), and a live node **never** self-gossips `offline`. (Bound-gated subtlety: an **unbound** perch reads `is_perch_alive==false` yet has a live broker session — it is still *warm*, so it is `active`/`dormant`, not suspended.) _Also avoid_: treating **corrupt** (a present-but-destroyed perch record) as a fifth instance state — it is a **record condition**, not a liveness state. A corrupt …\n\r\n**effective instance state**: The instance state a reader acts on is always **derived** — liveness discriminates warm/cold (with the unbound subtlety above), and the stored rest intent (`dormant` vs `suspended`) refines only *within* warm. No single stored field is authoritative for \"resting-cold\"; any reader that trusts a stored rest field against observed liveness (or vice versa) is wrong by construction. _Avoid_: treating the rest-intent record as cold-truth, or defaulting an absent intent to *active*.\r\n\r\nThe endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.\r\n\r\n### Agent endpoints vs Shells\r\n\r\nEndpoint types split into two families:\r\n\r\n**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.\r\n\r\n…\n\n…\nThe CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default → prior adapters → \"choose a different adapter\") → *home subnet* / other creation prompts as needed → launch; a **\"+ new endpoint\"** entry → the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a co…","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,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":72413,"outputLines":300,"outputBytes":72413,"shownRange":{"start":1,"end":300},"nextOffset":301},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195736478}}
{"type":"message","id":"19408cfb","parentId":"f7199b28","timestamp":"2026-07-16T09:55:36.520Z","message":{"role":"toolResult","toolCallId":"call_msUQmsgOctlX1F5N2SyLaHZ6|fc_0cc7a0c9a797f232016a58aa9859a4819a9eb7710de6a124bc","toolName":"read","content":[{"type":"text","text":"# Known Hazards\n\nHard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.\n\n**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.\n\n---\n\n## 1. Race conditions & ordering\n\n### 1.1 Phantom INIT_SIGNOFF after grace period\n- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\n- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\n- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\n- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\n\n### 1.2 Poll-rewrite race & info.json mid-write reads\n- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n\n### 1.3 Stale `index.lock` wedge from prior git crash\n- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\n- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\n- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n\n### 1.4 Deferred spool rows must not leak to the event stream\n- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\n- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\n- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\n\n### 1.5 Worker (working-perch) lifecycle path consistency\n- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\n- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\n- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n\n---\n\n## 2. Identity & session-binding\n\n### 2.1 Parent PID over ephemeral poll PID\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n\n### 2.2 Stdin session_id precedence over env\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n\n### 2.3 Binary-handoff argv schema must stay backward-compatible\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n\n---\n\n## 3. Lifecycle\n\n### 3.1 Ephemeral perch cleanup on every `ring` exit path\n- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\n- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\n- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\n- **Sister cite:** `src/owl/ring.rs:58-294`.\n\n### 3.2 Stale signoff sentinel must not kill a fresh start\n- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\n- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\n- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n\n### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\n- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\n- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\n\n---\n\n## 4. Wire / transport\n\n### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST\n- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).\n- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.\n- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.\n- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\n- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.\n\n### 4.2 Two-slice envelope parser is panic-free and tolerant\n- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\n- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.\n- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.\n- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.\n\n### 4.3 Registry stale-entry cleanup precedes lookup\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n\n### 4.4 Deferred rows survive poll drain\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\n\n### 4.5 Inbox legacy compat must not double-deliver\n- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.\n- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.\n- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve \"never read at drain time.\"\n- **Sister cite:** `src/common/inbox.rs`.\n\n### 4.6 Addressable-id charset reserves the address delimiters\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\n\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\n\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n\n### 4.9 SQLite stores must create their parent dir — SQLite won't\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\n\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\n\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\n- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).\n- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).\n- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\n- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\n\n---\n\n## 5. Platform-specific\n\n### 5.1 Windows PID recycling false positives\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\n\n### 5.2 Windows EBUSY on atomic rename\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n\n### 5.3 Git/subprocess timeout stamping\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\n- **Sister cite:** `src/common/git.rs`.\n\n### 5.4 Windows UNC prefix in serialized paths\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\n- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\n- **Sister cite:** `src/common/owlery.rs:377-384`.\n\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\n\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\n\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\n- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\nvm4w\\nodejs\\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.\n- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spaw…\n- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\n- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\n\n<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->\n### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\n- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).\n- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope …\n- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\n\n<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\n### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\n- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\n- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\n- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\n\n<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\n### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened c…\n- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.\n- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n\n<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->\n### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\n- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != \"live_agent\"` (or `controllable != Some(true)`) and *silently `continue`s* — th…\n- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1…\n- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue,…\n- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n\n---\n\n## 6. Documented regressions (non-obvious invariants)\n\n### 6.1 No flat/nested perch siblings; resolver-routed paths\n- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\n- **Invariant:** one path resolver; never create divergent siblings.\n- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\n- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.\n\n### 6.2 Soft-cleanup preserves state, removes `ready`\n- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\n- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\n- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\n- **Sister cite:** `src/owl/stop.rs`.\n\n…\n\n…\n<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->\n\n[Showing lines 1-300 of 736. Use :301 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"# Known Hazards\r\n\r\nHard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.\r\n\r\n**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.\r\n\r\n---\r\n\r\n## 1. Race conditions & ordering\r\n\r\n### 1.1 Phantom INIT_SIGNOFF after grace period\r\n- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\r\n- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\r\n- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\r\n\r\n### 1.2 Poll-rewrite race & info.json mid-write reads\r\n- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\r\n- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\r\n- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\r\n- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\r\n\r\n### 1.3 Stale `index.lock` wedge from prior git crash\r\n- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\r\n- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\r\n- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\r\n- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\r\n\r\n### 1.4 Deferred spool rows must not leak to the event stream\r\n- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\r\n- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\r\n- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\r\n- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\r\n\r\n### 1.5 Worker (working-perch) lifecycle path consistency\r\n- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\r\n- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\r\n- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\r\n- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\r\n\r\n---\r\n\r\n## 2. Identity & session-binding\r\n\r\n### 2.1 Parent PID over ephemeral poll PID\r\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\r\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\r\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\r\n\r\n### 2.2 Stdin session_id precedence over env\r\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\r\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\r\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\r\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\r\n\r\n### 2.3 Binary-handoff argv schema must stay backward-compatible\r\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\r\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\r\n- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\r\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\r\n\r\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\r\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\r\n- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\r\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\r\n- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\r\n\r\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\r\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\r\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\r\n\r\n---\r\n\r\n## 3. Lifecycle\r\n\r\n### 3.1 Ephemeral perch cleanup on every `ring` exit path\r\n- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\r\n- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\r\n- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\r\n- **Sister cite:** `src/owl/ring.rs:58-294`.\r\n\r\n### 3.2 Stale signoff sentinel must not kill a fresh start\r\n- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\r\n- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\r\n- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\r\n\r\n### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\r\n- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\r\n- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\r\n- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\r\n\r\n---\r\n\r\n## 4. Wire / transport\r\n\r\n### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST\r\n- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).\r\n- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.\r\n- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.\r\n- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\r\n- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.\r\n\r\n### 4.2 Two-slice envelope parser is panic-free and tolerant\r\n- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\r\n- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.\r\n- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.\r\n- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.\r\n\r\n### 4.3 Registry stale-entry cleanup precedes lookup\r\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\r\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\r\n- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\r\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\r\n\r\n### 4.4 Deferred rows survive poll drain\r\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\r\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\r\n\r\n### 4.5 Inbox legacy compat must not double-deliver\r\n- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.\r\n- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.\r\n- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve \"never read at drain time.\"\r\n- **Sister cite:** `src/common/inbox.rs`.\r\n\r\n### 4.6 Addressable-id charset reserves the address delimiters\r\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\r\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\r\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\r\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\r\n\r\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\r\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\r\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\r\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\r\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\r\n\r\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\r\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\r\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\r\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\r\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\r\n\r\n### 4.9 SQLite stores must create their parent dir — SQLite won't\r\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\r\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\r\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\r\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\r\n\r\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\r\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\r\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\r\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\r\n\r\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\r\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\r\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\r\n- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).\r\n- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).\r\n- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\r\n- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\r\n\r\n---\r\n\r\n## 5. Platform-specific\r\n\r\n### 5.1 Windows PID recycling false positives\r\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\r\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\r\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\r\n\r\n### 5.2 Windows EBUSY on atomic rename\r\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\r\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\r\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\r\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\r\n\r\n### 5.3 Git/subprocess timeout stamping\r\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\r\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\r\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\r\n- **Sister cite:** `src/common/git.rs`.\r\n\r\n### 5.4 Windows UNC prefix in serialized paths\r\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\r\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\r\n- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\r\n- **Sister cite:** `src/common/owlery.rs:377-384`.\r\n\r\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\r\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\r\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\r\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\r\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\r\n\r\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\r\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\r\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\r\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\r\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\r\n\r\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\r\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\r\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\r\n\r\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\r\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\r\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\r\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\r\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\r\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\r\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\r\n\r\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\r\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\r\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\r\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\r\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\r\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\r\n\r\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\r\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\r\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\r\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\r\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\r\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\r\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\r\n\r\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\r\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\r\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\r\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\r\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\r\n\r\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\r\n### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\r\n- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\nvm4w\\nodejs\\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.\r\n- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spaw…\n- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\r\n- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\r\n\r\n<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->\r\n### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\r\n- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).\r\n- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope …\n- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\r\n\r\n<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\r\n### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\r\n- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\r\n- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\r\n- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\r\n\r\n<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\r\n### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\r\n- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened c…\n- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.\r\n- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\r\n- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\r\n\r\n<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->\r\n### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\r\n- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != \"live_agent\"` (or `controllable != Some(true)`) and *silently `continue`s* — th…\n- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1…\n- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue,…\n- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\r\n\r\n---\r\n\r\n## 6. Documented regressions (non-obvious invariants)\r\n\r\n### 6.1 No flat/nested perch siblings; resolver-routed paths\r\n- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\r\n- **Invariant:** one path resolver; never create divergent siblings.\r\n- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\r\n- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.\r\n\r\n### 6.2 Soft-cleanup preserves state, removes `ready`\r\n- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\r\n- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\r\n- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\r\n- **Sister cite:** `src/owl/stop.rs`.\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":736,"totalBytes":57468,"outputLines":300,"outputBytes":57468,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# Known Hazards\r\n\r\nHard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.\r\n\r\n**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.\r\n\r\n---\r\n\r\n## 1. Race conditions & ordering\r\n\r\n### 1.1 Phantom INIT_SIGNOFF after grace period\r\n- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\r\n- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\r\n- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\r\n\r\n### 1.2 Poll-rewrite race & info.json mid-write reads\r\n- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\r\n- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\r\n- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\r\n- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\r\n\r\n### 1.3 Stale `index.lock` wedge from prior git crash\r\n- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\r\n- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\r\n- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\r\n- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\r\n\r\n### 1.4 Deferred spool rows must not leak to the event stream\r\n- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\r\n- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\r\n- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\r\n- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\r\n\r\n### 1.5 Worker (working-perch) lifecycle path consistency\r\n- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\r\n- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\r\n- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\r\n- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\r\n\r\n---\r\n\r\n## 2. Identity & session-binding\r\n\r\n### 2.1 Parent PID over ephemeral poll PID\r\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\r\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\r\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\r\n\r\n### 2.2 Stdin session_id precedence over env\r\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\r\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\r\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\r\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\r\n\r\n### 2.3 Binary-handoff argv schema must stay backward-compatible\r\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\r\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\r\n- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\r\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\r\n\r\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\r\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\r\n- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\r\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\r\n- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\r\n\r\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\r\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\r\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\r\n\r\n---\r\n\r\n## 3. Lifecycle\r\n\r\n### 3.1 Ephemeral perch cleanup on every `ring` exit path\r\n- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\r\n- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\r\n- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\r\n- **Sister cite:** `src/owl/ring.rs:58-294`.\r\n\r\n### 3.2 Stale signoff sentinel must not kill a fresh start\r\n- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\r\n- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\r\n- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\r\n\r\n### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\r\n- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\r\n- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\r\n- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\r\n\r\n---\r\n\r\n## 4. Wire / transport\r\n\r\n### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST\r\n- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).\r\n- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.\r\n- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.\r\n- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\r\n- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.\r\n\r\n### 4.2 Two-slice envelope parser is panic-free and tolerant\r\n- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\r\n- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.\r\n- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.\r\n- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.\r\n\r\n### 4.3 Registry stale-entry cleanup precedes lookup\r\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\r\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\r\n- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\r\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\r\n\r\n### 4.4 Deferred rows survive poll drain\r\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\r\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\r\n\r\n### 4.5 Inbox legacy compat must not double-deliver\r\n- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.\r\n- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.\r\n- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve \"never read at drain time.\"\r\n- **Sister cite:** `src/common/inbox.rs`.\r\n\r\n### 4.6 Addressable-id charset reserves the address delimiters\r\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\r\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\r\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\r\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\r\n\r\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\r\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\r\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\r\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\r\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\r\n\r\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\r\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\r\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\r\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\r\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\r\n\r\n### 4.9 SQLite stores must create their parent dir — SQLite won't\r\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\r\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\r\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\r\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\r\n\r\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\r\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\r\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\r\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\r\n\r\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\r\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\r\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\r\n- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).\r\n- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).\r\n- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\r\n- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\r\n\r\n---\r\n\r\n## 5. Platform-specific\r\n\r\n### 5.1 Windows PID recycling false positives\r\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\r\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\r\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\r\n\r\n### 5.2 Windows EBUSY on atomic rename\r\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\r\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\r\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\r\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\r\n\r\n### 5.3 Git/subprocess timeout stamping\r\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\r\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\r\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\r\n- **Sister cite:** `src/common/git.rs`.\r\n\r\n### 5.4 Windows UNC prefix in serialized paths\r\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\r\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\r\n- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\r\n- **Sister cite:** `src/common/owlery.rs:377-384`.\r\n\r\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\r\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\r\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\r\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\r\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\r\n\r\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\r\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\r\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\r\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\r\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\r\n\r\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\r\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\r\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\r\n\r\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\r\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\r\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\r\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\r\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\r\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\r\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\r\n\r\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\r\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\r\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\r\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\r\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\r\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\r\n\r\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\r\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\r\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\r\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\r\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\r\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\r\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\r\n\r\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\r\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\r\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\r\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\r\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\r\n\r\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\r\n### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\r\n- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\nvm4w\\nodejs\\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.\r\n- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spaw…\n- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\r\n- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\r\n\r\n<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->\r\n### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\r\n- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).\r\n- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope …\n- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\r\n\r\n<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\r\n### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\r\n- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\r\n- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\r\n- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\r\n\r\n<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\r\n### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\r\n- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened c…\n- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.\r\n- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\r\n- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\r\n\r\n<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->\r\n### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\r\n- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != \"live_agent\"` (or `controllable != Some(true)`) and *silently `continue`s* — th…\n- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1…\n- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue,…\n- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\r\n\r\n---\r\n\r\n## 6. Documented regressions (non-obvious invariants)\r\n\r\n### 6.1 No flat/nested perch siblings; resolver-routed paths\r\n- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\r\n- **Invariant:** one path resolver; never create divergent siblings.\r\n- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\r\n- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.\r\n\r\n### 6.2 Soft-cleanup preserves state, removes `ready`\r\n- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\r\n- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\r\n- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\r\n- **Sister cite:** `src/owl/stop.rs`.\r\n\r\n…\n\n…\n<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,null,364,null,735]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":736,"totalBytes":57468,"outputLines":300,"outputBytes":57468,"shownRange":{"start":1,"end":300},"nextOffset":301},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195736519}}
{"type":"message","id":"f1bfc0cc","parentId":"19408cfb","timestamp":"2026-07-16T09:55:36.560Z","message":{"role":"toolResult","toolCallId":"call_rgzsCOMS8u8nHxMIVq6rKAZE|fc_0cc7a0c9a797f232016a58aa985994819a8bca666bf84a1e2e","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 24|\n*25|While verifying the v0.3.2 cross-OS update fix end-to-end on the real fleet, the third node (`enlyzeam`, a Windows box hosting the `gerald` ready_agent) was updated:\n 26|\n*27|- `spt update apply` returned `APPLIED:6`.\n 28|- The installed `spt.exe` on disk was the valid 0.3.2 Windows PE (magic `4d5a`, sha matched the published binary), `--version` reported `0.3.2`, `applied.json` = `{version:6}`, backup `spt.exe.old-6` present.\n 29|- **But the running daemon's pid was unchanged, and it was still executing 0.3.0 in memory.**\n 30|\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 40|\n 41|Walking that question backward surfaced the actual root cause (Section 2), which reframed the whole task from \"add a feature\" to \"correct a regression.\"\n 42|\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## CONTEXT-MEMORY.md\n 34|\n*35|### `spt refresh` (spt-hosted self-service context refresh)\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 5|Status: **BUILT (M8-D4, decision 19).** The watcher shipped as specified:\n*6|status-only query on the update wire (`UpdRecord::StatusQuery`/`Status`,\n 7|served by `propagate::serve_update` under the same trust gate as a pull),\n 8|`propagate::classify_status` as the pure classifier, the poll loop + table\n 9|in `xtask debug-converge`. Deviations from the spec below: the per-node\n...\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...\n 81|\n*82|1. **Reuse the update pull/serve handshake** (`propagate.rs`). The serve leg\n 83|   already answers \"what do you have?\" during a pull. Add a *status-only*\n 84|   query (no fetch) that returns `{channel, applied_version, last_outcome}` for\n*85|   the requester's platform. Cheapest: it rides the existing update wire\n*86|   (`net/update.rs`) and the QUIC-handshake-identity origin rule (KH 7.5) gives\n 87|   the responder's identity for free.\n 88|2. **Registry/presence enrichment.** If a node already publishes its applied\n 89|   version + provenance into a registry row (the rollout metadata carries\n...\n 104|  node-a   Applied                v7\n*105|  node-b   StagedAwaitingConsent  v6  (run: spt update apply)\n 106|  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)\n 107|CONVERGED 1/3  (timeout 120s)\n 108|```\n...\n 117|1. **Status query (wire).** Add the status-only request/response to\n*118|   `net/update.rs` + the daemon serve path in `propagate.rs`; origin identity\n 119|   from the stream table (KH 7.5), never payload bytes. Unit-test the\n 120|   request/response codec.\n 121|2. **Node enumeration.** Factor the trust-derived subnet membership read out of\n## DEBUG-ROLLOUT.md\n 5|Debug rollout is the maintainer-only fast path for testing a local spt-core\n*6|build across a trusted lab subnet. It uses the normal signed self-update\n 7|substrate: no raw peer file-copy, no production `spt` CLI surface, and no\n 8|embedded debug trust anchor.\n 9|\n...\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 54|\n*55|From the fast coordinator, stage an update set into the local release cache:\n 56|\n 57|```powershell\n 58|cargo run -p xtask -- debug-rollout --build-current --artifact \"x86_64-unknown-linux-gnu=<path-to-linux-spt>\"\n...\n 71|\n*72|The staged update then propagates through the normal pull-based update pump.\n 73|There is intentionally no bespoke push path.\n 74|\n 75|## Apply and observe\n...\n 79|```powershell\n*80|spt update apply\n 81|```\n 82|\n*83|Nodes configured for full-auto update may apply after staging without a prompt.\n*84|For quick debugging, run or poke the daemon/update pump on lab nodes so they\n 85|query peers promptly.\n 86|\n 87|**Convergence watcher (M8-D4, decision 19).** Instead of hand-walking nodes,\n...\n 94|It polls every expected node (the coordinator's trust rows; `--nodes <hex,…>`\n*95|overrides) over the status-only update query and prints a per-node table —\n 96|`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n 97|`Offline` — exiting `0` only when every node applied the target (`1`\n 98|otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\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...\n 115|   `spt` CLI/help surface.\n*116|3. Keep verification on the production update substrate: signed metadata,\n 117|   per-platform artifact digest, channel pinning, monotonic version, and\n 118|   apply-time re-verification.\n*119|4. Add or update traceability evidence for `REQ-UPD-6`.\n*120|5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.\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...\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 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 25|\n*26|**Messaging · Live-agent lifecycle · Terminal hosting · Networking (P2P + pairing) · Harness contract (manifest + `api`) · Instances · Shells · Self-update.**\n 27|\n 28|## Killer quickstart targets (two, one per audience)\n 29|\n...\n 88|- [Live-agent lifecycle](…/lifecycle/llms.txt): skeleton → live → dormant → suspended.\n*89|- … Terminal · Networking · Harness contract · Instances · Shells · Self-update\n 90|\n 91|## Reference (auto-generated)\n 92|- [spt-proto](https://docs.rs/spt-proto) · [spt-runtime](https://docs.rs/spt-runtime) · [spt-msg](https://docs.rs/spt-msg)\n...\n 122|**Vertical ordering (dependency-sequenced):**\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...\n 140|/// The manifest's `min_spt_core_version` exceeds this binary.\n*141|/// **Fix:** update spt-core, or pin the adapter to a compatible release.\n 142|VersionTooOld { required: Version, found: Version },\n 143|```\n 144|\n...\n 146|\n*147|Top-level `--help` groups by audience (`AGENT COMMANDS` send/ring/ready/poll · `HARNESS COMMANDS` api … · `NODE COMMANDS` pair/subnet/update). Per-command: **USAGE · FLAGS · EXAMPLES (real values) · LEARN MORE**. Beyond gh: a **`spt <cmd> --help --json`** structured-help mode so dev-agents parse the contract without scraping, plus gh's uniform **`--json <fields>` / `--jq` / `--template`** trio on any list command. The `--json` help export is generated + CI-gated alongside rustdoc and the schema.\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...\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...\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 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...\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 84|- Classification probes (all CLI-observable; no daemon change needed to\n*85|  detect): child pid alive? broker `has_session` still true? ready marker\n 86|  present? →\n 87|  - `WEDGED_PRE_BIND` — pid alive, session alive, never bound (modes 1, 2)\n 88|  - `EXITED_PRE_BIND` — child exited before binding (mode 5-shaped recipes\n 89|    that pass argv but die fast; exit detail when capturable)\n*90|  - `NO_SESSION` — broker session vanished mid-wait (mode 6 shape)\n 91|- Deadline: **default 60 s** (field-confirmed: real binds land t+5–15 s;\n 92|  every observed bringup exceeding ~20 s was a wedge that never bound — the\n 93|  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n...\n 95|  belongs to the adapter) and per-invocation via `--bind-deadline <secs>`\n*96|  (the per-node escape for cold-broker first-spawn / loaded boxes). Escape\n 97|  hatch `--no-wait` restores fire-and-forget and prints\n 98|  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n 99|- The attach (non-`--start`) path upgrades the same way: `await_endpoint_session`\n...\n 111|CC dying at the trust dialog, so trust is durable across resumes only if the\n*112|same root+seed persists — exactly the seed-2 fragility): a command the broker\n 113|runs **synchronously, before the session spawn**, with:\n 114|\n 115|1. **Env parity via capture-at-creation (BINDING; sharpened by perri review):**\n 116|   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n*117|   at endpoint creation and stamped into the perch record**; the broker threads\n 118|   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119|   spawn. Neither consumer reads its own process env for a stamped var — if the\n 120|   seeder read its own env while the spawn read the stamp (or vice versa),\n...\n 208|   every >20 s bringup was a wedge, never a slow success). Floor 30 s; per-node\n*209|   override kept for cold-broker first-spawn / loaded boxes. Folded into §3.\n 210|2. **`pre_spawn` on resume — ANSWERED: YES.** Resume re-checks trust (INT-2:\n 211|   resumed CC died at the trust dialog). Folded into §3 Half B.\n 212|3. **Mode-6 — ANSWERED: FIXED** by F-028 B1/B4, 0.23.0 definitive repro\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 59|  doesn't Windows-image-lock the adapter registry dir during `spt adapter\n*60|  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the\n 61|  image only for a bounded turn — the own-copy machinery loses its reason.\n 62|- Psyche TURNS already work daemon-driven today (`psyche_download`, revival\n 63|  prompt, reconcile merge) — proof the per-event shape is the natural one.\n...\n 164|`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n*165|image-locked at update time). Compat: a manifest that still names the\n 166|own-copy path resolves install-dir-first anyway (REQ-INSTALL-11 unchanged).\n 167|\n 168|### W6 — docs\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 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*39|| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n*40|| 21 | **Stream lost mid-update-artifact** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's staged release (staging is atomic, artifact-then-metadata commit point — a torn pull stages nothing); the scratch file is transient | No resume protocol by design: re-query fresh — the query is idempotent and chunks are positional | torn-pull shape shared with row 14 (`request_update` errors on EOF-before-`Done`); staging atomicity: `relcache.rs::torn_or_cor...\n 41|\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...\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...\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## INJECT-SETTLE-REARM-GATE-VERDICT.md\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## KNOWN-HAZARDS.md\n 4|\n\n[Showing lines 1-364 of 725 (49.8KB limit). Use :365 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"src, tests, docs, CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":189,"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/INJECT-SETTLE-REARM-GATE-VERDICT.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/MSG-IDENTITY-DISPATCH.md","docs/MUSL-RELEASE-HANDOFF.md","docs/MUSL-W1-DISPATCH-RULING.md","docs/MUSL-W3-DISPATCH-RULING.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md"],"fileMatches":[{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":20},{"path":"docs/CONTEXT-MEMORY.md","count":2},{"path":"docs/DEBUG-CONVERGE-PLAN.md","count":7},{"path":"docs/DEBUG-ROLLOUT.md","count":13},{"path":"docs/DEFERRED.md","count":15},{"path":"docs/DOCS-STRATEGY.md","count":6},{"path":"docs/DORMANCY-BUDGET.md","count":5},{"path":"docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","count":9},{"path":"docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","count":2},{"path":"docs/F-035-CONFLICT.md","count":1},{"path":"docs/FAULT-MATRIX.md","count":18},{"path":"docs/FLAKE-LEDGER.md","count":7},{"path":"docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","count":1},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/MANIFEST.md","count":20},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":5},{"path":"docs/MUSL-RELEASE-HANDOFF.md","count":5},{"path":"docs/MUSL-W1-DISPATCH-RULING.md","count":5},{"path":"docs/MUSL-W3-DISPATCH-RULING.md","count":8},{"path":"docs/NEXT-MILESTONE-BUG-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 24│\n*25│While verifying the v0.3.2 cross-OS update fix end-to-end on the real fleet, the third node (`enlyzeam`, a Windows box hosting the `gerald` ready_agent) was updated:\n 26│\n*27│- `spt update apply` returned `APPLIED:6`.\n 28│- The installed `spt.exe` on disk was the valid 0.3.2 Windows PE (magic `4d5a`, sha matched the published binary), `--version` reported `0.3.2`, `applied.json` = `{version:6}`, backup `spt.exe.old-6` present.\n 29│- **But the running daemon's pid was unchanged, and it was still executing 0.3.0 in memory.**\n 30│\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 40│\n 41│Walking that question backward surfaced the actual root cause (Section 2), which reframed the whole task from \"add a feature\" to \"correct a regression.\"\n 42│\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## CONTEXT-MEMORY.md\n 34│\n*35│### `spt refresh` (spt-hosted self-service context refresh)\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   5│Status: **BUILT (M8-D4, decision 19).** The watcher shipped as specified:\n  *6│status-only query on the update wire (`UpdRecord::StatusQuery`/`Status`,\n   7│served by `propagate::serve_update` under the same trust gate as a pull),\n   8│`propagate::classify_status` as the pure classifier, the poll loop + table\n   9│in `xtask debug-converge`. Deviations from the spec below: the per-node\n    │...\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    │...\n  81│\n *82│1. **Reuse the update pull/serve handshake** (`propagate.rs`). The serve leg\n  83│   already answers \"what do you have?\" during a pull. Add a *status-only*\n  84│   query (no fetch) that returns `{channel, applied_version, last_outcome}` for\n *85│   the requester's platform. Cheapest: it rides the existing update wire\n *86│   (`net/update.rs`) and the QUIC-handshake-identity origin rule (KH 7.5) gives\n  87│   the responder's identity for free.\n  88│2. **Registry/presence enrichment.** If a node already publishes its applied\n  89│   version + provenance into a registry row (the rollout metadata carries\n    │...\n 104│  node-a   Applied                v7\n*105│  node-b   StagedAwaitingConsent  v6  (run: spt update apply)\n 106│  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)\n 107│CONVERGED 1/3  (timeout 120s)\n 108│```\n    │...\n 117│1. **Status query (wire).** Add the status-only request/response to\n*118│   `net/update.rs` + the daemon serve path in `propagate.rs`; origin identity\n 119│   from the stream table (KH 7.5), never payload bytes. Unit-test the\n 120│   request/response codec.\n 121│2. **Node enumeration.** Factor the trust-derived subnet membership read out of\n## DEBUG-ROLLOUT.md\n   5│Debug rollout is the maintainer-only fast path for testing a local spt-core\n  *6│build across a trusted lab subnet. It uses the normal signed self-update\n   7│substrate: no raw peer file-copy, no production `spt` CLI surface, and no\n   8│embedded debug trust anchor.\n   9│\n    │...\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  54│\n *55│From the fast coordinator, stage an update set into the local release cache:\n  56│\n  57│```powershell\n  58│cargo run -p xtask -- debug-rollout --build-current --artifact \"x86_64-unknown-linux-gnu=<path-to-linux-spt>\"\n    │...\n  71│\n *72│The staged update then propagates through the normal pull-based update pump.\n  73│There is intentionally no bespoke push path.\n  74│\n  75│## Apply and observe\n    │...\n  79│```powershell\n *80│spt update apply\n  81│```\n  82│\n *83│Nodes configured for full-auto update may apply after staging without a prompt.\n *84│For quick debugging, run or poke the daemon/update pump on lab nodes so they\n  85│query peers promptly.\n  86│\n  87│**Convergence watcher (M8-D4, decision 19).** Instead of hand-walking nodes,\n    │...\n  94│It polls every expected node (the coordinator's trust rows; `--nodes <hex,…>`\n *95│overrides) over the status-only update query and prints a per-node table —\n  96│`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n  97│`Offline` — exiting `0` only when every node applied the target (`1`\n  98│otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\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    │...\n 115│   `spt` CLI/help surface.\n*116│3. Keep verification on the production update substrate: signed metadata,\n 117│   per-platform artifact digest, channel pinning, monotonic version, and\n 118│   apply-time re-verification.\n*119│4. Add or update traceability evidence for `REQ-UPD-6`.\n*120│5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.\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   │...\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 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  25│\n *26│**Messaging · Live-agent lifecycle · Terminal hosting · Networking (P2P + pairing) · Harness contract (manifest + `api`) · Instances · Shells · Self-update.**\n  27│\n  28│## Killer quickstart targets (two, one per audience)\n  29│\n    │...\n  88│- [Live-agent lifecycle](…/lifecycle/llms.txt): skeleton → live → dormant → suspended.\n *89│- … Terminal · Networking · Harness contract · Instances · Shells · Self-update\n  90│\n  91│## Reference (auto-generated)\n  92│- [spt-proto](https://docs.rs/spt-proto) · [spt-runtime](https://docs.rs/spt-runtime) · [spt-msg](https://docs.rs/spt-msg)\n    │...\n 122│**Vertical ordering (dependency-sequenced):**\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    │...\n 140│/// The manifest's `min_spt_core_version` exceeds this binary.\n*141│/// **Fix:** update spt-core, or pin the adapter to a compatible release.\n 142│VersionTooOld { required: Version, found: Version },\n 143│```\n 144│\n    │...\n 146│\n*147│Top-level `--help` groups by audience (`AGENT COMMANDS` send/ring/ready/poll · `HARNESS COMMANDS` api … · `NODE COMMANDS` pair/subnet/update). Per-command: **USAGE · FLAGS · EXAMPLES (real values) · LEARN MORE**. Beyond gh: a **`spt <cmd> --help --json`** structured-help mode so dev-agents parse the contract without scraping, plus gh's uniform **`--json <fields>` / `--jq` / `--template`** trio on any list command. The `--json` help export is generated + CI-gated alongside rustdoc and the schema.\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   │...\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   │...\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  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    │...\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  84│- Classification probes (all CLI-observable; no daemon change needed to\n *85│  detect): child pid alive? broker `has_session` still true? ready marker\n  86│  present? →\n  87│  - `WEDGED_PRE_BIND` — pid alive, session alive, never bound (modes 1, 2)\n  88│  - `EXITED_PRE_BIND` — child exited before binding (mode 5-shaped recipes\n  89│    that pass argv but die fast; exit detail when capturable)\n *90│  - `NO_SESSION` — broker session vanished mid-wait (mode 6 shape)\n  91│- Deadline: **default 60 s** (field-confirmed: real binds land t+5–15 s;\n  92│  every observed bringup exceeding ~20 s was a wedge that never bound — the\n  93│  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n    │...\n  95│  belongs to the adapter) and per-invocation via `--bind-deadline <secs>`\n *96│  (the per-node escape for cold-broker first-spawn / loaded boxes). Escape\n  97│  hatch `--no-wait` restores fire-and-forget and prints\n  98│  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n  99│- The attach (non-`--start`) path upgrades the same way: `await_endpoint_session`\n    │...\n 111│CC dying at the trust dialog, so trust is durable across resumes only if the\n*112│same root+seed persists — exactly the seed-2 fragility): a command the broker\n 113│runs **synchronously, before the session spawn**, with:\n 114│\n 115│1. **Env parity via capture-at-creation (BINDING; sharpened by perri review):**\n 116│   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n*117│   at endpoint creation and stamped into the perch record**; the broker threads\n 118│   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119│   spawn. Neither consumer reads its own process env for a stamped var — if the\n 120│   seeder read its own env while the spawn read the stamp (or vice versa),\n    │...\n 208│   every >20 s bringup was a wedge, never a slow success). Floor 30 s; per-node\n*209│   override kept for cold-broker first-spawn / loaded boxes. Folded into §3.\n 210│2. **`pre_spawn` on resume — ANSWERED: YES.** Resume re-checks trust (INT-2:\n 211│   resumed CC died at the trust dialog). Folded into §3 Half B.\n 212│3. **Mode-6 — ANSWERED: FIXED** by F-028 B1/B4, 0.23.0 definitive repro\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n  59│  doesn't Windows-image-lock the adapter registry dir during `spt adapter\n *60│  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the\n  61│  image only for a bounded turn — the own-copy machinery loses its reason.\n  62│- Psyche TURNS already work daemon-driven today (`psyche_download`, revival\n  63│  prompt, reconcile merge) — proof the per-event shape is the natural one.\n    │...\n 164│`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n*165│image-locked at update time). Compat: a manifest that still names the\n 166│own-copy path resolves install-dir-first anyway (REQ-INSTALL-11 unchanged).\n 167│\n 168│### W6 — docs\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 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*39│| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n*40│| 21 | **Stream lost mid-update-artifact** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's staged release (staging is atomic, artifact-then-metadata commit point — a torn pull stages nothing); the scratch file is transient | No resume protocol by design: re-query fresh — the query is idempotent and chunks are positional | torn-pull shape shared with row 14 (`request_update` errors on EOF-before-`Done`); staging atomicity: `relcache.rs::torn_or_cor...\n 41│\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   │...\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   │...\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## INJECT-SETTLE-REARM-GATE-VERDICT.md\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## 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  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 146│- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n*147│- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-...\n 148│- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n 149│\n 150│### 4.9 SQLite stores must create their parent dir — SQLite won't\n    │...\n 158│- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the...\n*159│- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future upd...\n 160│- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n 161│- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n*162│- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transiti...\n 163│\n 164│### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\n 165│<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\n*166│- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\n 167│- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the...\n 168│- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction...\n 169│- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\n    │...\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*183│- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n 184│- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n*185│- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 186│- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 187│\n 188│### 5.3 Git/subprocess timeout stamping\n    │...\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    │...\n 252│- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\n...\n*253│- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvabl...\n 254│- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\n 255│- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\n 256│\n    │...\n 258│### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\n*259│- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes ...\n 260│- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes...\n 261│- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Read...\n*262│- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\n 263│\n 264│<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\n 265│### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\n*266│- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\n 267│- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\n 268│- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLI...\n*269│- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\n 270│\n 271│<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\n 272│### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n## MANIFEST.md\n  14│- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).\n *15│- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.\n  16│- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.\n *17│- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).\n  18│\n  19│<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the\n  20│typed form in `crates/spt-runtime/src/manifest.rs` is kept in lockstep. -->\n    │...\n  28│version = \"2.0.0\"\n *29│min_spt_core_version = \"1.0.0\"     # readable before any update; compat gate\n  30│hostable_types = [\"LiveAgent\", \"ReadyAgent\", \"Psyche\", \"Worker\"]\n  31│shortcut_basename = \"cc\"           # optional; brands the picker's launcher → cc-<id> (default \"spt\")\n  32│host_binaries = [\"claude\"]         # optional; the harness exe(s) this adapter hosts agents inside\n    │...\n  36│`version` is **mandatory**. It is the single declared adapter version — read\n *37│before any update (the `gh_release` compare point), surfaced by `spt adapter\n  38│version <name>` (which prints this `[adapter].version` of a registered adapter;\n *39│exit 1 if unregistered), and the value the post-update `message` gate keys on.\n  40│There is no second version source — no `[strings].version`, no `get-string`\n  41│convention.\n  42│\n    │...\n  44│`min_spt_core_version` is the **enforced** compatibility floor. Both acquisition\n *45│verbs — `spt adapter add` and `spt adapter update` — REFUSE when the installed\n  46│spt-core is below this version, naming the installed core, the floor, and the next\n *47│action (update spt-core first). The check is a numeric per-component compare (so\n  48│`0.9.0 < 0.25.0`), and it fires **before** anything is written: a refused add leaves\n *49│the registry untouched, and a refused update leaves the live install byte-untouched.\n  50│The enforcement is unconditional — there is **no** opt-in flag (the former\n *51│`[update].version_check` knob was retired; it was never read).\n  52│\n  53│<!-- [doc->REQ-MANIFEST-7] -->\n  54│`shortcut_basename` *(optional, default `spt`)* — the basename the `spt endpoint\n    │...\n  71│`claude.cmd`, *and* the renamed-in-use form `claude.exe.old.<timestamp>` (a harness\n *72│self-update can rename its still-running exe, e.g. Claude Code's updater, so a\n  73│long-lived session's exe basename is `claude.exe.old.<ts>`). **A declared\n  74│host-binary name must therefore not itself contain a dot** (real harness exe names\n  75│don't). With `host_binaries`, `spt api seed` / `listen` need **no `--adapter`** —\n    │...\n 169│|---|---|---|\n*170│| `{adapter_dir}` | the registry record's `source_dir` — the adapter's **install dir** (`adapters/_github/<safe>/` for a `--release`/`--github` adapter; the copy-mode `source_dir` otherwise) | the same dir bare-program resolution already uses (REQ-INSTALL-11); **survives updates** (a `gh_release` re-extract re-registers in place), so a path built from it stays valid across `spt adapter update` |\n 171│| `{adapter_name}` | the resolved `[adapter].name` | identical to the catalog `{adapter_name}` above |\n 172│\n 173│Because they carry no session/event context, an adapter can build a path to its **own packed binary** and have spt-core *resolve* it without spt-core ever executing it — spt-core substitutes and **returns** the string; the adapter's own wrapper runs the result (resolve-not-execute; see ADR-0029). The canonical use is hook dispatch: an adapter stores a command template in `[strings]` (below), reads it once per session with `spt adapter get-string`, and runs the resolved command per-hook from a thin stat...\n    │...\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 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 319│```\n*320│Pointer files live in `adapters/<adapter>/strings/`, referenced by a path that **must stay inside** that dir (`..`/absolute refused at registration). Validated at register (fail-fast), read lazily at get-string (live edits reflect), skip-diagnoses a missing file at read (mirrors `[digest]`). A **local** profile's pointers resolve against the user-owned local-profile dir (update-safe). Published reference: [manifest.md `[strings]`](../docs-site/src/harness-contract/manifest.md#strings--adapter-string-valu...\n 321│\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    │...\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 349│every PTY write). Idle-only; busy/mid-turn delivery stays adapter hook-injection.\n    │...\n 358│<!-- [doc->REQ-TRANSLATE-COMMAND] -->\n*359│- **`command` (preferred, since v0.16.0)** — an **opaque** command string (a program token plus args), exactly like the other command seams. Its program token resolves against the adapter **install dir** (REQ-INSTALL-11), like `[digest].extractor` / `[session.psyche_resume]`: a bare/relative program (e.g. `claude-spt`) resolves to `<install_dir>/<program>(.exe)` before PATH. Args support **adapter-static `{adapter_dir}` / `{adapter_name}` substitution only** — **not** session keys. The translation bi...\n 360│- **`path` (deprecated)** — the original bare-program form. It still **parses** (forward/back-compat) but **warns at registration**, steering authors to `command`. **Exactly one of `{path, command}`** may be set: both-set is **refused** at registration; neither set = **no translation binary** (the endpoint falls back to its `[inject]` hook path).\n*361│- Whichever form is used, the resolved program is spawned at the initial spt-hosted bringup **and** at a live-update respawn. (A declared binary that fails to spawn is logged `TRANSLATION_SPAWN_FAILED` on the daemon's stderr; before diagnosing idle-delivery behavior, confirm the binary actually spawned.) <!-- [doc->REQ-INSTALL-11] -->\n 362│- **stdin** (spt-core → binary, one JSON object per line): `{\"type\":\"init\",\"endpoint_id\":…,\"node\":…}` first · `{\"type\":\"event\",\"envelope\":\"<EVENT…>\"}` per inbound message (the ADR-0020 envelope) · `{\"type\":\"input\"}` a **content-free** ping each time the operator types (so the binary can track user-idle; the PTY input content is **never** duplicated to the binary).\n 363│- **stdout** (binary → spt-core, one per line): `{\"key\":\"ctrl+s\"}` · `{\"delay_ms\":50}` · `{\"text\":\"<payload>\"}` · `{\"key\":\"enter\"}` · **`{\"commit\":true}`**, … (extensible vocabulary).\n 364│- **`{\"commit\":true}` is the mandatory sequence TERMINATOR, required for EVERY `{\"type\":\"event\"}`.** While a sequence is in flight spt-core buffers the live `spt rc` controller's keystrokes (the *inject floor*) and applies the emitted commands atomically; the trailing `{\"commit\":true}` releases the floor and flushes the buffered controller input *after* the sequence. The submit keystroke is NOT the terminator (a choreography may keep typing after `{\"key\":\"enter\"}`). **An empty response is a protocol viol...\n    │...\n 452│  `seq` has fallen **out** of the window (the consumer fell behind a slide), the\n*453│  response is a **full-window refresh plus a predates signal** (mirrors the\n*454│  version-slide full-refresh).\n 455│- **`partial: true`** — the trailing **in-progress** turn (not yet closed by a\n 456│  following user-input) is flagged `partial`, and its entries carry **no stable\n 457│  `seq`** until the turn closes (the turn is the commit unit; entries can still\n## MSG-IDENTITY-DISPATCH.md\n 38│- `REQ-LISTEN-SESSION-ID-FALLBACK` [doc,impl,unit,int] — `listen --session-id <sid>` binds when\n*39│  the ephemeral seed is gone (late go-live / post-daemon-restart). Doc stage pins seed lifetime.\n 40│- Roots pinned (scope F-034-sptc-live-core-legs.md). Field-verify: hertz + perri.\n 41│\n 42│### W3 — INGEST-DURABILITY (F-032; data-loss)\n   │...\n 72│  daemon start. Deployah field-confirmed the gap live (mobile-gw alive=false after the v0.30.6\n*73│  restart). Field-verify: flynn (their logon-task interim dissolves).\n 74│- `REQ-DAEMON-SERVICE-INSTALL` stays QUEUED (rider, infra-provisioning milestone).\n 75│\n 76│### W6 — POISON-LOG-SCOPE (F-039; deployah v0.30.6 PASS handoff, **RCA-first**)\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   │...\n 95│  census = field symptoms across all six waves (dup deliveries, seat-theft, lost communes,\n*96│  bringup dead-ends, gateway-down-after-restart, poison log floods).\n 97│\n 98│<!-- Co-authored by: doyle -->\n## MUSL-RELEASE-HANDOFF.md\n  5│## What's gated (don't re-gate the code — verify the PUBLISH)\n *6│- release.yml musl matrix (kitsubito, `CC_x86_64_unknown_linux_musl=musl-gcc`, in-job musl-tools+cmake+target); assemble globs `spt-x86_64-*` into SHA256SUMS + upload; xtask signs + update-set-enters musl by registry-derived triple (zero xtask code change post-W1).\n *7│- **Proven manually on kitsubito:** static-pie musl binary (no glibc), self-IDs `x86_64-unknown-linux-musl`, live fetch correctly rejects `NoArtifactForPlatform(musl)` PRE-publish. Int test pins update-set select+verify-over-musl-bytes.\n  8│\n  9│## Drive the release\n 10│1. On the W3 branch: bump VERSION + Cargo.lock first-party ([[release-Cargo.lock-first-party]]); counter from PUBLISHED spt-releases metadata ([[release-counter-from-published]]) — last was v0.29.1=counter 50, so expect **v0.30.0 = counter 51** (minor: additive new platform), CONFIRM from published not memory.\n*11│2. CHANGELOG: user-facing surface = \"static-musl Linux artifact (`spt-x86_64-linux-musl`) — runs on pre-glibc-2.39 hosts; `spt update fetch` now selects+verifies it. gnu stays the default Linux artifact.\" Scope by observable surface ([[changelog-scope-vs-commit-range]]).\n 12│3. Release PR (milestone+release, [[release-standard-bump-in-pr]]), CI both legs green, merge → tag on merge.\n 13│4. Tag fires release.yml — **the FIRST real CI musl build happens here** (the musl matrix runs on tag, not PR). De-risked: I built it clean on kitsubito. Watch the kitsubito musl leg; if it flakes on CI-runner contention, [[seedmap-test-collides-live-daemon]] (kitsubito = musl-build box AND gnu runner) — sequence/sweep, rerun `--failed` (no re-tag).\n*14│5. **Close the field gap for real:** after publish, on a musl node run `spt update fetch` → must now get the musl artifact (NO NoArtifactForPlatform) → verify SHA256+sig over musl bytes → apply → self-updates → self-IDs musl at the new version. THAT is the full self-update E2E completing on the published release (my gate proved every leg upstream of the published artifact).\n 15│\n 16│## Watch-fors\n*17│- update-set stale-serve window ([[release-updateset-stale-window]]): upload-before-flip covers the musl entry.\n 18│- CI flake NOT to be spooked by: `inject_control_wedge::wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` is a Windows-box-timing flake on the 80ms inject settle-window (untouched by W3, PASS on kitsubito, green in v0.29.1). If the hfenduleam test leg trips it, sweep leaked `spt.exe` (scoped) + rerun `--failed` — do NOT read it as a musl regression.\n 19│\n 20│Questions → doyle. gnu is unchanged; musl is purely additive.\n## MUSL-W1-DISPATCH-RULING.md\n  7│- **CONTEXT.md:828** — novel Linux platforms (handhelds) foreseen; \"relocatable binary + minimal, non-OS-entangled install.\" (W2/W3 deliver the musl artifact; W1 is the enabling refactor.)\n *8│- **ADR-0016** (platform-targeted update sets) + **CONTEXT.md:575** — per-platform artifact set is the model; `NoArtifactForPlatform` is *designed* behaviour for a missing platform.\n  9│\n 10│## Ruling — registry shape: **const-table, NOT enum**\n 11│One authoritative table; everything derives from it. Rationale: adding a platform = adding one row (CONTEXT:3 \"structurally easy\"), no match-arm sprawl across methods.\n   │...\n 24│- **asset-name ↔ triple map** (`xtask/src/main.rs:598-599`) → derive from the table (lookup by `asset_name`).\n*25│- Test literals (`cli.rs:16922-923`, release.rs/update.rs/net-update.rs) — leave as-is unless they block; they assert on the existing two triples, which are unchanged.\n 26│\n 27│## Behaviour-neutral — the gate IS the unchanged green suite\n*28│gnu + windows behaviour **byte-identical**: same triples, same asset names, same `current_platform()` outputs. No new platform this wave (musl is W2). The existing **release / update / apply / propagate** test suites stay green — that is the proof of neutrality, not a new test (beyond the `current_platform ∈ registry` unit + a registry-projection unit).\n 29│\n 30│## Gate criteria (doyle)\n 31│- clippy `--workspace --all-targets -D warnings`; `traceable-reqs check --json` exit 0 (REQ-PLATFORM-REGISTRY +impl +unit).\n*32│- Full `nextest --workspace` BOTH legs (Windows hfenduleam + Linux kitsubito) — release/update/apply/propagate suites unchanged + green.\n 33│- Diff review: no behaviour change for gnu/windows; `SUPPORTED_PLATFORMS` is the single source; applyhost generalized correctly (N-platform, not hardcoded pair).\n 34│- A musl build is NOT required for W1 (that's W2) — but the tree must still compile for a musl target (the `\"unknown\"` fallback stays until W2 adds the arm).\n 35│\n 36│## Sequencing\n*37│W1 (this) → gate → **W2** (register musl: `current_platform()` musl cfg arm + the musl `PlatformSpec` row + asset map; DNS/crypto/build already proven) → gate → **W3** (CI musl build + sign + update-set publish + self-update E2E on a sub-2.39-glibc box) → gate → release (deployah publishes). doyle drives the whole arc.\n## MUSL-W3-DISPATCH-RULING.md\n  5│## What W3 closes (the field gap)\n *6│A musl-built `spt` now self-identifies as `x86_64-unknown-linux-musl` (W2) — so today it `spt update fetch` → `UPDATE_FETCH_REJECTED: NoArtifactForPlatform(musl)` because **no release carries a musl artifact**. W3 supplies that artifact end-to-end: CI build → SHA256SUMS → signed release upload → update-set entry → self-update works on a sub-2.39-glibc box.\n  7│\n  8│## CONTEXT / ADR grounding (checked — design-aligned)\n  9│- **CONTEXT.md:3** — platforms additive, \"not a re-architecture.\" musl is ADDITIVE; **gnu stays the default Linux artifact**, musl rides alongside.\n 10│- **CONTEXT.md:828** — novel Linux (handhelds, sub-glibc) foreseen: \"relocatable binary + minimal, non-OS-entangled install.\" The static musl binary IS that relocatable artifact.\n*11│- **ADR-0016** — platform-targeted update sets; the update-set names each platform's artifact + the recipient selects `current_platform()`'s. musl becomes one more named entry, no machinery change.\n 12│\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   │...\n 21│3. **release-publish (xtask)** — signs the musl artifact under the SAME whole-artifact Ed25519 flow (REQ-UPD-9 single-artifact verify); no per-platform signing divergence.\n*22│4. **update-set** — carries the musl artifact entry (`update-set.json` SignedRelease envelope per [[v0190-published]]) so a musl node's `fetch` selects it.\n 23│\n 24│## Gate criteria (doyle — release-pipeline touch → REAL E2E, no mocks)\n 25│- clippy `--workspace --all-targets -D warnings`; `traceable-reqs check --json` exit 0 (`REQ-RELEASE-MUSL-ARTIFACT` +impl +unit +int).\n*26│- Full `nextest --workspace` BOTH legs (Windows hfenduleam + Linux kitsubito) green — gnu/windows release/update/apply/propagate suites UNCHANGED (musl additive).\n 27│- **The int gate is a real cut**: assemble a **draft/test release** carrying the musl artifact, then on a **static musl binary running on a sub-2.39-glibc box**:\n*28│  - `spt update fetch` → gets the musl artifact (NO `NoArtifactForPlatform`)\n 29│  - verifies SHA256 + Ed25519 signature **over the musl bytes**\n 30│  - `apply` → self-updates → the re-launched binary self-reports `x86_64-unknown-linux-musl` at the fetched version.\n 31│- gnu default-Linux path unchanged: a gnu node still fetches `spt-x86_64-linux`, never the musl asset.\n   │...\n 36│## Watch-fors\n*37│- **`Win update-exe UAC-740`** is Windows-only — irrelevant to musl, but don't rename the musl asset into an installer-detected name.\n 38│- **CI runner contention** ([[seedmap-test-collides-live-daemon]]): kitsubito is the musl BUILD box AND a gnu CI runner — a musl build job racing a gnu test job on the same box can leak/contend. Sequence the matrix or scope the daemon sweep; a Phase-A e2e daemon-timeout on kitsubito is contamination, not a musl regression (W2 CI hit exactly this — rerun cleared it).\n*39│- update-set **stale-serve window** ([[release-updateset-stale-window.md]]): upload-before-flip holds for the musl entry too.\n 40│\n 41│Co-authored by: doyle\n## NEXT-MILESTONE-BUG-TRIAGE.md\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  43│sceltouin ↔ enlyzeam: `spt send` reported **SENT(WAN)** both ways but NOTHING delivered, even though `spt whoami`/endpoint list showed both agents to each other. **KEY LEAD:** sceltouin's IP **changed** since it joined the subnet; enlyzeam's did not. Strong signal: stale gossiped/pinned address → WAN dial to a dead address \"succeeds\" optimistically → false SENT(WAN). Tangential: SENT(WAN) must not be reported until delivery is actually confirmed (or it must reflect best-effort honestly).\n *44│- Root cause: _TBD (address refresh on IP change; SENT(WAN) optimism)_\n  45│- Fix approach: _TBD_\n  46│- Relates: [[broken-ipv6-poisons-iroh-discovery]] (address/path staleness class).\n  47│\n    │...\n  51│\n *52│### C-#1 — `spt adapter update` errors suppressed during `spt adapter add`  · STATUS: SHIPPED @9305d19 (W6, REQ-ADAPTER-ADD-SURFACE-ERRORS)\n *53│`adapter add` chains into `adapter update`, but update errors don't surface — swallowed (filter_map ok() / ignored Result on the chained call).\n  54│- Root cause: _TBD_\n  55│- Fix approach: _TBD_\n  56│\n    │...\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    │...\n 109│\n*110│**C-#1 adapter-update errors suppressed in adapter add** · conf H · effort S. `AdapterCmd::Add` inline (cli.rs:6975-7139); install=first-update → `conduct(\"INSTALL\",&cmd)`. ROOT: `conduct` Ok-failure arm (cli.rs:6963-6966) prints only `ADAPTER_INSTALL_FAIL: exit Some(1)`, DISCARDS out.stdout/stderr (sibling run_update_post_step :7592-7602 does it right). Exit code DOES propagate — only detail is lost. Secondary: verdict `Skipped`(7113)+catch-all(7124) return 0 unconditionally; gh_release `[update]`...\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    │...\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    │...\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*142│- **(a) CONTRIBUTING MISSING-MECHANISM — no address-refresh-on-IP-change; send bypasses warm cache** · conf M · effort L. ROOT: gossiped registry `Instance` carries NO address (wanmsg.rs:104-120) → IP change NEVER re-propagated via gossip; addressing = 100% iroh (mDNS/n0-DNS/relay) + local `PeerAddrStore` (peeraddrs.rs) refreshed ONLY reactively by peer pump `dial_seeded` (pump/mod.rs:694-713); writers = pairing (pairhost.rs:547)/roster gapfill (seedproofx.rs:933)/pump write-back. NO proactive \"my ...\n 143│\n*144│**B cross-cut:** #4 + #9/#10 both want the same primitive: resolve-owning-node → dial-by-id → round-trip-to-that-node's-broker. wansend.rs has the resolve+dial half. **Do #9/#10(b) FIRST** (receiver→sender reply, highest-leverage/lowest-risk, makes failure visible), then #4 cross-node rc reuses the helper, then (a) address-refresh.\n 145│\n 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*154│**A cross-cut:** #2 (stale rows counted) + #3 (missing source datum) are the same family — \"remote viewer derives wrong fact from gossip\" — but NEITHER is a freshness problem (node-refresh helps neither). #15 couples #11 (display).\n 155│\n 156│---\n 157│\n    │...\n 161│- **Gossip-truth (M):** A-#3 (separate any-controller datum) + A-#2 secondary (Offline-TTL eviction).\n*162│- **WAN cluster (ordered):** B-#9/#10(b) reply-leg FIRST (makes loss visible) → B-#4 cross-node rc (reuse resolve→dial→round-trip) → B-#9/#10(a) address-refresh-on-IP-change (L).\n 163│- **PTY foundational (L):** D-#6 server-side VT grid → unlocks proper sticky marker redo (#8/#14) + kills residual artifacts (#7).\n*164│- **Design-gate first:** A-#15 model-change (if wanted) needs CONTEXT.md amendment; C-#1 decide if install-time runs [update.post].\n 165│\n 166│---\n 167│\n    │...\n 172│### E-#16 — eel-a (sole live endpoint, hfenduleam) can no longer be attached  · STATUS: TRIAGE\n*173│`spt rc eel-a` → `RC_FAIL:eel-a: attach request: brain IPC read deadline elapsed`. Worked shortly after creation. **LIVE EVIDENCE (doyle, this box):** endpoint list/daemon show eel-a `ready=true alive=true` (alive is BIND-gated — set at api bind, does NOT reflect live brain health), yet the attach IPC times out → the brain isn't servicing IPC (wedged/dead) while the perch still reads alive. daemon pid 19180. **STRONG LEAD:** the v0.18.0 `spt update apply` daemon/brain RESPAWN (done on this box earl...\n*174│- **Root cause (agent E, conf M-mechanism-H): broker-resident shared-state corruption from the self-update brain-RESPAWN — NOT the brain itself.** The only deadline-bounded call in request_attach is `net_open_stream` (rc.rs:1086, awaits NetStreamOpened under ~10s); error minted brain.rs:1442 = broker's per-conn serve thread sent NO frame for KIND_NET_STREAM_OPEN. DISCRIMINATOR: on the same rc conn, KIND_SESSIONS + KIND_NET_DIAL_LOOPBACK already SUCCEEDED (broker pid 19180 alive, thread reached attach) ...\n 175│\n 176│### G-#17 — no digest for eel-a  · STATUS: SHIPPED CLOSED — W6 env-read @9305d19 (REQ-DIGEST-PROFILE-ENV) + W6b fetcher-strategy @b9e5ce9 (REQ-DIGEST-FETCHER-STRATEGY)\n 177│> **W6** shipped the harness-agnostic `[env] direction=\"read\"` seam (capture-at-bind → info.json.read_env → digest fill_template keys → `value` fallback → `~` expansion → skip-diagnose). **W6b** closed the `{project}` design-gate per doyle's CONTEXT-symmetry ruling: `[digest]` gained a `fetcher` strategy mirroring `[history]`'s — the ADAPTER's extractor locates+reads+emits (no spt-core pre-read, no `{project}` slug), fed only harness-neutral `{session_id}`+`{cwd}`(=info.json.cwd)+captured rea...","missingPaths":["packages","test"],"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 24|\n*25|While verifying the v0.3.2 cross-OS update fix end-to-end on the real fleet, the third node (`enlyzeam`, a Windows box hosting the `gerald` ready_agent) was updated:\n 26|\n*27|- `spt update apply` returned `APPLIED:6`.\n 28|- The installed `spt.exe` on disk was the valid 0.3.2 Windows PE (magic `4d5a`, sha matched the published binary), `--version` reported `0.3.2`, `applied.json` = `{version:6}`, backup `spt.exe.old-6` present.\n 29|- **But the running daemon's pid was unchanged, and it was still executing 0.3.0 in memory.**\n 30|\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 40|\n 41|Walking that question backward surfaced the actual root cause (Section 2), which reframed the whole task from \"add a feature\" to \"correct a regression.\"\n 42|\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## CONTEXT-MEMORY.md\n 34|\n*35|### `spt refresh` (spt-hosted self-service context refresh)\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 5|Status: **BUILT (M8-D4, decision 19).** The watcher shipped as specified:\n*6|status-only query on the update wire (`UpdRecord::StatusQuery`/`Status`,\n 7|served by `propagate::serve_update` under the same trust gate as a pull),\n 8|`propagate::classify_status` as the pure classifier, the poll loop + table\n 9|in `xtask debug-converge`. Deviations from the spec below: the per-node\n...\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...\n 81|\n*82|1. **Reuse the update pull/serve handshake** (`propagate.rs`). The serve leg\n 83|   already answers \"what do you have?\" during a pull. Add a *status-only*\n 84|   query (no fetch) that returns `{channel, applied_version, last_outcome}` for\n*85|   the requester's platform. Cheapest: it rides the existing update wire\n*86|   (`net/update.rs`) and the QUIC-handshake-identity origin rule (KH 7.5) gives\n 87|   the responder's identity for free.\n 88|2. **Registry/presence enrichment.** If a node already publishes its applied\n 89|   version + provenance into a registry row (the rollout metadata carries\n...\n 104|  node-a   Applied                v7\n*105|  node-b   StagedAwaitingConsent  v6  (run: spt update apply)\n 106|  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)\n 107|CONVERGED 1/3  (timeout 120s)\n 108|```\n...\n 117|1. **Status query (wire).** Add the status-only request/response to\n*118|   `net/update.rs` + the daemon serve path in `propagate.rs`; origin identity\n 119|   from the stream table (KH 7.5), never payload bytes. Unit-test the\n 120|   request/response codec.\n 121|2. **Node enumeration.** Factor the trust-derived subnet membership read out of\n## DEBUG-ROLLOUT.md\n 5|Debug rollout is the maintainer-only fast path for testing a local spt-core\n*6|build across a trusted lab subnet. It uses the normal signed self-update\n 7|substrate: no raw peer file-copy, no production `spt` CLI surface, and no\n 8|embedded debug trust anchor.\n 9|\n...\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 54|\n*55|From the fast coordinator, stage an update set into the local release cache:\n 56|\n 57|```powershell\n 58|cargo run -p xtask -- debug-rollout --build-current --artifact \"x86_64-unknown-linux-gnu=<path-to-linux-spt>\"\n...\n 71|\n*72|The staged update then propagates through the normal pull-based update pump.\n 73|There is intentionally no bespoke push path.\n 74|\n 75|## Apply and observe\n...\n 79|```powershell\n*80|spt update apply\n 81|```\n 82|\n*83|Nodes configured for full-auto update may apply after staging without a prompt.\n*84|For quick debugging, run or poke the daemon/update pump on lab nodes so they\n 85|query peers promptly.\n 86|\n 87|**Convergence watcher (M8-D4, decision 19).** Instead of hand-walking nodes,\n...\n 94|It polls every expected node (the coordinator's trust rows; `--nodes <hex,…>`\n*95|overrides) over the status-only update query and prints a per-node table —\n 96|`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n 97|`Offline` — exiting `0` only when every node applied the target (`1`\n 98|otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\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...\n 115|   `spt` CLI/help surface.\n*116|3. Keep verification on the production update substrate: signed metadata,\n 117|   per-platform artifact digest, channel pinning, monotonic version, and\n 118|   apply-time re-verification.\n*119|4. Add or update traceability evidence for `REQ-UPD-6`.\n*120|5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.\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...\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 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 25|\n*26|**Messaging · Live-agent lifecycle · Terminal hosting · Networking (P2P + pairing) · Harness contract (manifest + `api`) · Instances · Shells · Self-update.**\n 27|\n 28|## Killer quickstart targets (two, one per audience)\n 29|\n...\n 88|- [Live-agent lifecycle](…/lifecycle/llms.txt): skeleton → live → dormant → suspended.\n*89|- … Terminal · Networking · Harness contract · Instances · Shells · Self-update\n 90|\n 91|## Reference (auto-generated)\n 92|- [spt-proto](https://docs.rs/spt-proto) · [spt-runtime](https://docs.rs/spt-runtime) · [spt-msg](https://docs.rs/spt-msg)\n...\n 122|**Vertical ordering (dependency-sequenced):**\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...\n 140|/// The manifest's `min_spt_core_version` exceeds this binary.\n*141|/// **Fix:** update spt-core, or pin the adapter to a compatible release.\n 142|VersionTooOld { required: Version, found: Version },\n 143|```\n 144|\n...\n 146|\n*147|Top-level `--help` groups by audience (`AGENT COMMANDS` send/ring/ready/poll · `HARNESS COMMANDS` api … · `NODE COMMANDS` pair/subnet/update). Per-command: **USAGE · FLAGS · EXAMPLES (real values) · LEARN MORE**. Beyond gh: a **`spt <cmd> --help --json`** structured-help mode so dev-agents parse the contract without scraping, plus gh's uniform **`--json <fields>` / `--jq` / `--template`** trio on any list command. The `--json` help export is generated + CI-gated alongside rustdoc and the schema.\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...\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...\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 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...\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 84|- Classification probes (all CLI-observable; no daemon change needed to\n*85|  detect): child pid alive? broker `has_session` still true? ready marker\n 86|  present? →\n 87|  - `WEDGED_PRE_BIND` — pid alive, session alive, never bound (modes 1, 2)\n 88|  - `EXITED_PRE_BIND` — child exited before binding (mode 5-shaped recipes\n 89|    that pass argv but die fast; exit detail when capturable)\n*90|  - `NO_SESSION` — broker session vanished mid-wait (mode 6 shape)\n 91|- Deadline: **default 60 s** (field-confirmed: real binds land t+5–15 s;\n 92|  every observed bringup exceeding ~20 s was a wedge that never bound — the\n 93|  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n...\n 95|  belongs to the adapter) and per-invocation via `--bind-deadline <secs>`\n*96|  (the per-node escape for cold-broker first-spawn / loaded boxes). Escape\n 97|  hatch `--no-wait` restores fire-and-forget and prints\n 98|  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n 99|- The attach (non-`--start`) path upgrades the same way: `await_endpoint_session`\n...\n 111|CC dying at the trust dialog, so trust is durable across resumes only if the\n*112|same root+seed persists — exactly the seed-2 fragility): a command the broker\n 113|runs **synchronously, before the session spawn**, with:\n 114|\n 115|1. **Env parity via capture-at-creation (BINDING; sharpened by perri review):**\n 116|   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n*117|   at endpoint creation and stamped into the perch record**; the broker threads\n 118|   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119|   spawn. Neither consumer reads its own process env for a stamped var — if the\n 120|   seeder read its own env while the spawn read the stamp (or vice versa),\n...\n 208|   every >20 s bringup was a wedge, never a slow success). Floor 30 s; per-node\n*209|   override kept for cold-broker first-spawn / loaded boxes. Folded into §3.\n 210|2. **`pre_spawn` on resume — ANSWERED: YES.** Resume re-checks trust (INT-2:\n 211|   resumed CC died at the trust dialog). Folded into §3 Half B.\n 212|3. **Mode-6 — ANSWERED: FIXED** by F-028 B1/B4, 0.23.0 definitive repro\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 59|  doesn't Windows-image-lock the adapter registry dir during `spt adapter\n*60|  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the\n 61|  image only for a bounded turn — the own-copy machinery loses its reason.\n 62|- Psyche TURNS already work daemon-driven today (`psyche_download`, revival\n 63|  prompt, reconcile merge) — proof the per-event shape is the natural one.\n...\n 164|`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n*165|image-locked at update time). Compat: a manifest that still names the\n 166|own-copy path resolves install-dir-first anyway (REQ-INSTALL-11 unchanged).\n 167|\n 168|### W6 — docs\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 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*39|| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n*40|| 21 | **Stream lost mid-update-artifact** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's staged release (staging is atomic, artifact-then-metadata commit point — a torn pull stages nothing); the scratch file is transient | No resume protocol by design: re-query fresh — the query is idempotent and chunks are positional | torn-pull shape shared with row 14 (`request_update` errors on EOF-before-`Done`); staging atomicity: `relcache.rs::torn_or_cor...\n 41|\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...\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...\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## INJECT-SETTLE-REARM-GATE-VERDICT.md\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## KNOWN-HAZARDS.md\n 4|","truncated":true,"truncatedBy":"bytes","totalLines":725,"totalBytes":99767,"outputLines":364,"outputBytes":50990,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":725,"totalBytes":99767,"outputLines":364,"outputBytes":50990,"shownRange":{"start":1,"end":364},"nextOffset":365}}},"isError":false,"timestamp":1784195736560}}
{"type":"message","id":"ac070c38","parentId":"f1bfc0cc","timestamp":"2026-07-16T09:55:48.338Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Mapping Rust crates**"},{"type":"toolCall","id":"call_e0Xv9opRBBkjRUw9pm2LE9yB|fc_0cc7a0c9a797f232016a58aaa44bc4819a9d91bc739d9e10fc","name":"glob","arguments":{"i":"Mapping Rust lifecycle files","path":"crates/**/*.rs; tests/**/*.rs","hidden":true,"gitignore":true,"limit":1000}},{"type":"toolCall","id":"call_pUQqapKqql72Lk6PWCZZQS7C|fc_0cc7a0c9a797f232016a58aaa44bd0819a9c1931743a9a1e4e","name":"grep","arguments":{"i":"Tracing daemon refresh command","pattern":"Daemon.*Refresh|daemon.*refresh|cmd_daemon|Refresh|refresh_brain|BrainRefresh|refresh","path":"crates","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_ws6K4CS1GwrZ01qCkLARfXZY|fc_0cc7a0c9a797f232016a58aaa44bdc819ab2ac37cd913be987","name":"grep","arguments":{"i":"Tracing update apply command","pattern":"cmd_update|UpdateCommand|Update.*Apply|apply_update|apply_staged|ApplyHost|applyhost","path":"crates","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_4fxqz3VVDTNQlqYRkgrV559Z|fc_0cc7a0c9a797f232016a58aaa44be4819aab826dc23c9504a8","name":"grep","arguments":{"i":"Finding brain restart supervision","pattern":"spawn_brain|restart_brain|supervis|StartReason|Refresh|BrainReady|BrainExit|handoff|refresh","path":"crates/spt-daemon","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_PAu9xla4yN4gYcYkl3QRXuRe|fc_0cc7a0c9a797f232016a58aaa44be8819a87a2affbb44f991d","name":"grep","arguments":{"i":"Finding process handle inheritance","pattern":"inherit|Stdio::|DETACHED_PROCESS|CREATE_NO_WINDOW|spawn_detached|current_exe|Command::new","path":"crates/spt-daemon;crates/spt","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_3GFhbuGTEbh1tI35ZBYrdVbj|fc_0cc7a0c9a797f232016a58aaa44bf0819a9026e2a1fb06fdd2","name":"read","arguments":{"i":"Reading split architecture ADR","path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","selector":null}},{"type":"toolCall","id":"call_dT0KqugUl0J4XT3XxtXbeQR6|fc_0cc7a0c9a797f232016a58aaa44bf4819a949152d3e046a2e2","name":"read","arguments":{"i":"Reading original split ADR","path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","selector":null}},{"type":"toolCall","id":"call_1FoYKBZ4mKeos5h5ACBmuv72|fc_0cc7a0c9a797f232016a58aaa44bfc819ab6f565afe3979da5","name":"read","arguments":{"i":"Reading coordinated update ADR","path":"docs/adr/0025-live-daemon-coordinated-adapter-update.md","selector":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":48467,"output":479,"cacheRead":13824,"cacheWrite":0,"totalTokens":62770,"reasoningTokens":20,"cost":{"input":0.24233500000000002,"output":0.01437,"cacheRead":0.006912,"cacheWrite":0,"total":0.263617}},"stopReason":"toolUse","timestamp":1784195736622,"responseId":"resp_0cc7a0c9a797f232016a58aa99c720819aacd8776064a8642b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cc7a0c9a797f232016a58aa9c7ae8819a90e5d405cbde0433","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqkgxq05YAA0pKqpHsew2-wLYY38YE-ayCRJqHNS4yGFTjVqi1HeS6RodK92tTdUY13yEuBOBJFQm5grUZlchdbb1lMKhy32NAm8_ffQhwZfiVDKqNSk8BwSgy9d-hv4I5FsL4yY-TTcKXI7Nbs8V_K4QznyaTFfsB8yL9-5abX7hNm_kXJA6Yy7D5bO5lEFBGMrJlMJ1L7JbZPJJggdSyiSfp647NK1KKlngWnR3zYwlgIZxzdczJPhVJyh2mRHQaRT8BGZ3o-RhXykp6SXQ2JivRRPgkPJWNSJUt-eThT47V69Y7BeFk2v_PaAnYQHW0-6PmqwQn3mTfz5ih3TQvIuW805UhI9ENkShxxJ5ANZ2fDsc5_mn4vMePabY7xJ-j-nBEXyDKCxNcZo753N9Eh1hycq9lAWyh32FnXolSLLIp8_WxEF3utug6LSVWkhbTOh_PPbECd7rUxAkTG9ykGVf76JMeMqjYIPyplAw-C3t9-LdAl3J3LbDgjAzFvRi830EsDotIbniMkDQ2yBOGCVytw9tMXHNy5otL5OlIffZpz9_XVPxn_zLhxq5GkWd0xQyLnB9_dZ48c3tqcNxnbHruJVNsSpVSZP09Mwz5Tyj4Dihz7Vw7K-x9d-wBfq6gVWQLoSbnlJy0ChYMZsByc8b6fmWUbAbMsRALoABsLBsXD2qPuby9pD1VHofD_6x9plm_idfhuiGgL4YVGiAgCfg7BDvEEoC32Vf06fKN32aC6YF8W52waK_387yOb2GslOOKWlMvQD2sI4mSqXXGbsMiZdcVtJLSuqhps94VmA1Sw6NneGT44Cd0OU7gSpluuPRfuYEyjg8IDxgUrNAXHNzbBjaFbUy0dntuzyDLmb6FEJpDMbGXCNvEuFs10Zs2mILbu1AZ0lJNREAfzOWZ0TVsRC-N0CjD4Gr9CibBsWnY9pz30Qs2Wa18sO0tBMXm41syRlB3Qzy-UsrpRbNm9CEjVoCnrh4Mrf-FiXLCq9Ds8bXNJsI6pV_imyH5JZeVQ","summary":[{"type":"summary_text","text":"**Mapping Rust crates**"}]},{"id":"fc_0cc7a0c9a797f232016a58aaa44bc4819a9d91bc739d9e10fc","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping Rust lifecycle files\",\"path\":\"crates/**/*.rs; tests/**/*.rs\",\"hidden\":true,\"gitignore\":true,\"limit\":1000}","call_id":"call_e0Xv9opRBBkjRUw9pm2LE9yB","name":"glob"},{"id":"fc_0cc7a0c9a797f232016a58aaa44bd0819a9c1931743a9a1e4e","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing daemon refresh command\",\"pattern\":\"Daemon.*Refresh|daemon.*refresh|cmd_daemon|Refresh|refresh_brain|BrainRefresh|refresh\",\"path\":\"crates\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_pUQqapKqql72Lk6PWCZZQS7C","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aaa44bdc819ab2ac37cd913be987","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing update apply command\",\"pattern\":\"cmd_update|UpdateCommand|Update.*Apply|apply_update|apply_staged|ApplyHost|applyhost\",\"path\":\"crates\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_ws6K4CS1GwrZ01qCkLARfXZY","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aaa44be4819aab826dc23c9504a8","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding brain restart supervision\",\"pattern\":\"spawn_brain|restart_brain|supervis|StartReason|Refresh|BrainReady|BrainExit|handoff|refresh\",\"path\":\"crates/spt-daemon\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_4fxqz3VVDTNQlqYRkgrV559Z","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aaa44be8819a87a2affbb44f991d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding process handle inheritance\",\"pattern\":\"inherit|Stdio::|DETACHED_PROCESS|CREATE_NO_WINDOW|spawn_detached|current_exe|Command::new\",\"path\":\"crates/spt-daemon;crates/spt\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_PAu9xla4yN4gYcYkl3QRXuRe","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aaa44bf0819a9026e2a1fb06fdd2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading split architecture ADR\",\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"selector\":null}","call_id":"call_3GFhbuGTEbh1tI35ZBYrdVbj","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aaa44bf4819a949152d3e046a2e2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading original split ADR\",\"path\":\"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md\",\"selector\":null}","call_id":"call_dT0KqugUl0J4XT3XxtXbeQR6","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aaa44bfc819ab6f565afe3979da5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading coordinated update ADR\",\"path\":\"docs/adr/0025-live-daemon-coordinated-adapter-update.md\",\"selector\":null}","call_id":"call_1FoYKBZ4mKeos5h5ACBmuv72","name":"read"}]},"duration":11713.630099999718,"ttft":3871.2527999999,"contextSnapshot":{"promptTokens":62291,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_e0Xv9opRBBkjRUw9pm2LE9yB|fc_0cc7a0c9a797f232016a58aaa44bc4819a9d91bc739d9e10fc","toolName":"glob","startedAt":"2026-07-16T09:55:48.339Z","args":{"path":"crates/**/*.rs; tests/**/*.rs"},"intent":"Mapping Rust lifecycle files"},"id":"2f8331a0","parentId":"ac070c38","timestamp":"2026-07-16T09:55:48.339Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pUQqapKqql72Lk6PWCZZQS7C|fc_0cc7a0c9a797f232016a58aaa44bd0819a9c1931743a9a1e4e","toolName":"grep","startedAt":"2026-07-16T09:55:48.339Z","args":{"path":"crates"},"intent":"Tracing daemon refresh command"},"id":"02a697c3","parentId":"2f8331a0","timestamp":"2026-07-16T09:55:48.339Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ws6K4CS1GwrZ01qCkLARfXZY|fc_0cc7a0c9a797f232016a58aaa44bdc819ab2ac37cd913be987","toolName":"grep","startedAt":"2026-07-16T09:55:48.340Z","args":{"path":"crates"},"intent":"Tracing update apply command"},"id":"51a8ae83","parentId":"02a697c3","timestamp":"2026-07-16T09:55:48.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4fxqz3VVDTNQlqYRkgrV559Z|fc_0cc7a0c9a797f232016a58aaa44be4819aab826dc23c9504a8","toolName":"grep","startedAt":"2026-07-16T09:55:48.340Z","args":{"path":"crates/spt-daemon"},"intent":"Finding brain restart supervision"},"id":"6c9324fe","parentId":"51a8ae83","timestamp":"2026-07-16T09:55:48.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PAu9xla4yN4gYcYkl3QRXuRe|fc_0cc7a0c9a797f232016a58aaa44be8819a87a2affbb44f991d","toolName":"grep","startedAt":"2026-07-16T09:55:48.340Z","args":{"path":"crates/spt-daemon;crates/spt"},"intent":"Finding process handle inheritance"},"id":"e6c7efd7","parentId":"6c9324fe","timestamp":"2026-07-16T09:55:48.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3GFhbuGTEbh1tI35ZBYrdVbj|fc_0cc7a0c9a797f232016a58aaa44bf0819a9026e2a1fb06fdd2","toolName":"read","startedAt":"2026-07-16T09:55:48.340Z","args":{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md"},"intent":"Reading split architecture ADR"},"id":"b2daa00a","parentId":"e6c7efd7","timestamp":"2026-07-16T09:55:48.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dT0KqugUl0J4XT3XxtXbeQR6|fc_0cc7a0c9a797f232016a58aaa44bf4819a949152d3e046a2e2","toolName":"read","startedAt":"2026-07-16T09:55:48.340Z","args":{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md"},"intent":"Reading original split ADR"},"id":"fb12ef4b","parentId":"b2daa00a","timestamp":"2026-07-16T09:55:48.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1FoYKBZ4mKeos5h5ACBmuv72|fc_0cc7a0c9a797f232016a58aaa44bfc819ab6f565afe3979da5","toolName":"read","startedAt":"2026-07-16T09:55:48.340Z","args":{"path":"docs/adr/0025-live-daemon-coordinated-adapter-update.md"},"intent":"Reading coordinated update ADR"},"id":"efed1e7a","parentId":"fb12ef4b","timestamp":"2026-07-16T09:55:48.340Z"}
{"type":"message","id":"7346e7f1","parentId":"efed1e7a","timestamp":"2026-07-16T09:55:48.344Z","message":{"role":"toolResult","toolCallId":"call_3GFhbuGTEbh1tI35ZBYrdVbj|fc_0cc7a0c9a797f232016a58aaa44bf0819a9026e2a1fb06fdd2","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":1784195748344}}
{"type":"message","id":"4885027f","parentId":"7346e7f1","timestamp":"2026-07-16T09:55:48.345Z","message":{"role":"toolResult","toolCallId":"call_dT0KqugUl0J4XT3XxtXbeQR6|fc_0cc7a0c9a797f232016a58aaa44bf4819a949152d3e046a2e2","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":1784195748345}}
{"type":"message","id":"fa9e6554","parentId":"4885027f","timestamp":"2026-07-16T09:55:48.345Z","message":{"role":"toolResult","toolCallId":"call_1FoYKBZ4mKeos5h5ACBmuv72|fc_0cc7a0c9a797f232016a58aaa44bfc819ab6f565afe3979da5","toolName":"read","content":[{"type":"text","text":"# Live, daemon-coordinated adapter update\n\n<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->\n\n## Status\n\naccepted (2026-06-22, v0.13.2) — built in W3; see the **Amendment (W3 build)** below for three corrections the build surfaced.\n\nExtends spt's seamless-self-update objective (ADR-0004, broker/brain split) from spt-core's own brain to **adapters**.\n\n## Context\n\n`spt adapter update` runs **CLI-side**: it fetches, optionally verifies, and re-registers the adapter on disk (REQ-UPD-7/9 deliberately keep HTTP off the daemon). It is **daemon-blind** — it never coordinates with the running daemon. Two facts make that unsafe while an endpoint is live:\n\n1. **A running endpoint caches the manifest in memory.** A `BrainLifecycle` clones the adapter `Manifest` at endpoint bringup (`lifecycle.rs`, the `manifest` field + the `ManifestRuntime` clone) and never re-reads it. After a disk update the in-memory manifest is **stale** — the running endpoint drives new binaries against an old manifest, or an old binary path against a new layout.\n\n2. **The daemon runs resident adapter binaries that hold file locks.** spt-core keeps a **resident adapter binary** alive for an endpoint's lifetime — today the `[message-idle-translation-binary]` (spawned when the spt-hosted endpoint comes up, killed when it goes down); historically, whatever adapter file the daemon held open. On Windows a running `.exe` cannot be overwritten, so the CLI update's `remove_dir_all` + extract fails with *\"Access is denied (os error 5)\"*. This is the locked-binary failure observed in the field.\n\nA blunt fix — refuse to update while any endpoint on the adapter is live (the stance ADR-0016 took for broker-touching core updates) — contradicts spt's core objective: on-the-fly updates. The brain already self-updates without terminating endpoints; adapters should too.\n\n## Decision\n\nWhen an adapter has **running resident binaries**, its update becomes **daemon-coordinated**. The CLI keeps fetch + verify (no new daemon HTTP surface); it then hands the **apply** to the daemon over IPC. The daemon apply, per affected endpoint:\n\n1. **Stop** the adapter's resident binaries → releases the OS file lock (fixes the Windows \"Access denied\" overwrite failure).\n2. **CRC-gated swap.** Replace on disk **only files whose CRC differs** from the staged archive; identical files (and their still-running binaries, when unchanged) are left untouched. Avoids needless rewrites and needless restarts.\n3. **Refresh the in-memory manifest.** Re-clone the new on-disk manifest into the running `BrainLifecycle`(s), so binaries and manifest are back on the same page.\n4. **Restart** the resident binaries from the new files.\n\nAn endpoint **not** running goes the simple path: the CLI swaps files directly — no lock, no cached manifest, no daemon round-trip.\n\n**Resident vs ephemeral.** Only the **resident** class — binaries spt-core keeps alive for an endpoint's lifetime (today: the translation binary; tomorrow: any future long-lived adapter-owned process) — is stopped and restarted. **Ephemeral** adapter binaries self-heal and are excluded: the psyche loop is daemon-hosted, not a separate process (ADR-0004); the `[digest]` extractor, `[session.*]` runners, and hooks are spawned on demand, so their next invocation simply execs the updated file. The daemon keeps a **per-endpoint registry of resident adapter children** so an apply knows exactly which live processes to cycle.\n\n**CRC** is plain content hashing of each archived file vs its installed counterpart; the goal is \"replace only what changed,\" not cryptographic integrity (that is the archive signature, ADR-0024 / REQ-UPD-9).\n\n## Considered Options\n\n- **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- **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- **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\n## Amendment (W3 build, 2026-06-22)\n\n<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->\n<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->\n\nThe build (gated wave-by-wave with doyle) surfaced three facts this ADR's Decision got wrong, plus a third axis to the resident/ephemeral split. The shipped design is the corrected one below.\n\n**Correction 1 — the Psyche is a separate install-dir binary, not in-daemon.** The Decision's \"Resident vs ephemeral\" excludes the Psyche as ephemeral *\"because the psyche loop is daemon-hosted, not a separate process (ADR-0004).\"* The field (claude-spt, perri **F-010×F-015**) proves otherwise: claude-spt's Psyche is a **separate, detached, unsupervised install-dir binary** (`[session.psyche_init] detach=true`, `Stdio::null`) that runs **straight from the shared install dir** and so **locks the update target** — its dead parent brain's orphan lingered ~4h holding the lock, and `spt adapter update` failed `tar exit 1` / *Access denied (os error 5)*.\n\n**Correction 2 — \"re-clone the manifest into the running `BrainLifecycle`\" is not implementable as written.** Decision step 3 assumes the apply can reach the live `BrainLifecycle`. It cannot: the `BrainLifecycle` (and the whole live-host + pulse loop) is **moved into a driver thread inside the `spt daemon brain` CHILD PROCESS**, while the broker — which runs the resident translation child and serves the apply IPC — is the **`spt daemon run` daemon PROCESS**. They share **no memory** (only the broker socket), so an in-memory manifest handle cannot be pushed across. **This also corrects the W3 design-gate verdict's \"broker registers the brain's `Arc` handle\"** — equally impossible. The shipped mechanism is **reload-on-dirty**: the brain's pulse loop (every per…\n\n**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 hash differs; torn down with the perch), never the shared install-dir binary — so it can never lock the update target, regardless of reap timing. REQ-INSTALL-11 holds: the bare program token still resolves to the adapter's shipped binary (identical content, only r…\n\n**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-on-dirty. Brain-parity holds: the endpoint / PTY / output log never restart — only the resident translation child cycles.\n\n## Amendment 2 (F015B, 2026-07-02) — matcher parity + never success-without-swap\n\n<!-- [doc->REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP] -->\n\nThe \"Net shipped flow\" above phrases the delegated apply as acting on \"affected endpoints\" but underspecified two invariants; the field bug **F015B / BUILD-F015B-APPLYMATCH** (a live update to a `--adapter cc:ccs` PROFILE-COMPOSITE endpoint silently no-oped — `LIVE`/`DONE` printed, version-of-truth stayed OLD) exposed both. Recorded here as binding, guarded by **REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP** (KNOWN-HAZARDS 7.24):\n\n**Invariant A — ONE parent-aware matcher across the live-update seam.** A `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` on its session / perch `info.adapter`, while an update carries the PARENT record name `cc`. Every live-update comparator — the CLI live-gate (`adapter_has_live_endpoint`), the broker apply's session filter (`dispatch_adapter_apply`), and the endpoint-selection (`select_endpoints_running_adapter`) — MUST match on the parent through the single shared `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option(session_adapter).0 == parent`). An EXACT `==` against a record name at ANY of these seams silently drops every `:profile` endpoint (→ `affected=[]`). The original build had the broker t…\n\n**Invariant B — the daemon owns the whole apply; never report success without swapping.** Once the CLI delegates the apply to the broker there is NO CLI-side fallback swap. The broker's CRC swap therefore runs UNCONDITIONALLY — the terminate/restart loops simply no-op when nothing is resident — and `KIND_APPLIED` is reported ONLY after a real swap. The original `affected.is_empty() → reply KIND_APPLIED + return` branch (an optimization: \"the endpoint just exited, nothing to coordinate\") was the amplifier that made Invariant A's skew a SILENT no-op; it is removed. A genuinely-exited endpoint still swaps the on-disk install dir (correct — the next spawn must read new bytes).\n\n## Amendment 3 (F-030 psyche-ephemeral, 2026-07-04) — the resident-Psyche premise is retired; `.live-bin` own-copy dissolved\n\n<!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] -->\n\nThe **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\n- **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 supervise, poll, or reap. This makes the Psyche's install-dir image lock **turn-bounded**, the *same accepted class as `[digest]`/`translate`/`[session.*]` today* — so the Psyche joins the **ephemeral** side of §\"Resident vs ephemeral\" outright, and Correction 1's \"excl…\n- **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 Correction 3's conclusion stands; only the Psyche's own-copy mechanism retires.\n- **Residency / thrash / reap machinery retired.** The `confirm_residency_or_unhost` residency arm (and the per-reconcile orphan reap of a resident Psyche, `REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP`) are removed with the resident child. A **one-shot legacy-resident sweep at brain start** replaces the ongoing reap — it kills only a pre-upgrade *stranded* resident wrapper (positive cmdline+basename match, fail-safe-decline, infra never-kill) and clears its stale `<parent>-psyche` ready registration; it is deliberately not per-reconcile/periodic (burying the residency era, not resurrecting it).\n- **A Psyche failure never touches the parent's hosted state.** Consecutive turn failures consume a bounded budget → a `psyche_host_error` stamp on the Self perch, never a teardown of the parent endpoint (KNOWN-HAZARDS 7.30/7.31, `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` / `REQ-HAZARD-THRASH-GUARD-BLIND`).\n\nNet: the daemon-coordinated apply is unchanged in shape — it still stops/CRC-swaps/restarts the **resident translation binary** — but the Psyche is no longer a coordination subject (nothing resident to stop), and the `.live-bin` relocation it once required is gone.\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"# Live, daemon-coordinated adapter update\r\n\r\n<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->\r\n\r\n## Status\r\n\r\naccepted (2026-06-22, v0.13.2) — built in W3; see the **Amendment (W3 build)** below for three corrections the build surfaced.\r\n\r\nExtends spt's seamless-self-update objective (ADR-0004, broker/brain split) from spt-core's own brain to **adapters**.\r\n\r\n## Context\r\n\r\n`spt adapter update` runs **CLI-side**: it fetches, optionally verifies, and re-registers the adapter on disk (REQ-UPD-7/9 deliberately keep HTTP off the daemon). It is **daemon-blind** — it never coordinates with the running daemon. Two facts make that unsafe while an endpoint is live:\r\n\r\n1. **A running endpoint caches the manifest in memory.** A `BrainLifecycle` clones the adapter `Manifest` at endpoint bringup (`lifecycle.rs`, the `manifest` field + the `ManifestRuntime` clone) and never re-reads it. After a disk update the in-memory manifest is **stale** — the running endpoint drives new binaries against an old manifest, or an old binary path against a new layout.\r\n\r\n2. **The daemon runs resident adapter binaries that hold file locks.** spt-core keeps a **resident adapter binary** alive for an endpoint's lifetime — today the `[message-idle-translation-binary]` (spawned when the spt-hosted endpoint comes up, killed when it goes down); historically, whatever adapter file the daemon held open. On Windows a running `.exe` cannot be overwritten, so the CLI update's `remove_dir_all` + extract fails with *\"Access is denied (os error 5)\"*. This is the locked-binary failure observed in the field.\r\n\r\nA blunt fix — refuse to update while any endpoint on the adapter is live (the stance ADR-0016 took for broker-touching core updates) — contradicts spt's core objective: on-the-fly updates. The brain already self-updates without terminating endpoints; adapters should too.\r\n\r\n## Decision\r\n\r\nWhen an adapter has **running resident binaries**, its update becomes **daemon-coordinated**. The CLI keeps fetch + verify (no new daemon HTTP surface); it then hands the **apply** to the daemon over IPC. The daemon apply, per affected endpoint:\r\n\r\n1. **Stop** the adapter's resident binaries → releases the OS file lock (fixes the Windows \"Access denied\" overwrite failure).\r\n2. **CRC-gated swap.** Replace on disk **only files whose CRC differs** from the staged archive; identical files (and their still-running binaries, when unchanged) are left untouched. Avoids needless rewrites and needless restarts.\r\n3. **Refresh the in-memory manifest.** Re-clone the new on-disk manifest into the running `BrainLifecycle`(s), so binaries and manifest are back on the same page.\r\n4. **Restart** the resident binaries from the new files.\r\n\r\nAn endpoint **not** running goes the simple path: the CLI swaps files directly — no lock, no cached manifest, no daemon round-trip.\r\n\r\n**Resident vs ephemeral.** Only the **resident** class — binaries spt-core keeps alive for an endpoint's lifetime (today: the translation binary; tomorrow: any future long-lived adapter-owned process) — is stopped and restarted. **Ephemeral** adapter binaries self-heal and are excluded: the psyche loop is daemon-hosted, not a separate process (ADR-0004); the `[digest]` extractor, `[session.*]` runners, and hooks are spawned on demand, so their next invocation simply execs the updated file. The daemon keeps a **per-endpoint registry of resident adapter children** so an apply knows exactly which live processes to cycle.\r\n\r\n**CRC** is plain content hashing of each archived file vs its installed counterpart; the goal is \"replace only what changed,\" not cryptographic integrity (that is the archive signature, ADR-0024 / REQ-UPD-9).\r\n\r\n## Considered Options\r\n\r\n- **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.\r\n- **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.\r\n- **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.\r\n\r\n## Amendment (W3 build, 2026-06-22)\r\n\r\n<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->\r\n<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->\r\n\r\nThe build (gated wave-by-wave with doyle) surfaced three facts this ADR's Decision got wrong, plus a third axis to the resident/ephemeral split. The shipped design is the corrected one below.\r\n\r\n**Correction 1 — the Psyche is a separate install-dir binary, not in-daemon.** The Decision's \"Resident vs ephemeral\" excludes the Psyche as ephemeral *\"because the psyche loop is daemon-hosted, not a separate process (ADR-0004).\"* The field (claude-spt, perri **F-010×F-015**) proves otherwise: claude-spt's Psyche is a **separate, detached, unsupervised install-dir binary** (`[session.psyche_init] detach=true`, `Stdio::null`) that runs **straight from the shared install dir** and so **locks the update target** — its dead parent brain's orphan lingered ~4h holding the lock, and `spt adapter update` failed `tar exit 1` / *Access denied (os error 5)*.\r\n\r\n**Correction 2 — \"re-clone the manifest into the running `BrainLifecycle`\" is not implementable as written.** Decision step 3 assumes the apply can reach the live `BrainLifecycle`. It cannot: the `BrainLifecycle` (and the whole live-host + pulse loop) is **moved into a driver thread inside the `spt daemon brain` CHILD PROCESS**, while the broker — which runs the resident translation child and serves the apply IPC — is the **`spt daemon run` daemon PROCESS**. They share **no memory** (only the broker socket), so an in-memory manifest handle cannot be pushed across. **This also corrects the W3 design-gate verdict's \"broker registers the brain's `Arc` handle\"** — equally impossible. The shipped mechanism is **reload-on-dirty**: the brain's pulse loop (every per…\n\r\n**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 hash differs; torn down with the perch), never the shared install-dir binary — so it can never lock the update target, regardless of reap timing. REQ-INSTALL-11 holds: the bare program token still resolves to the adapter's shipped binary (identical content, only r…\n\r\n**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-on-dirty. Brain-parity holds: the endpoint / PTY / output log never restart — only the resident translation child cycles.\r\n\r\n## Amendment 2 (F015B, 2026-07-02) — matcher parity + never success-without-swap\r\n\r\n<!-- [doc->REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP] -->\r\n\r\nThe \"Net shipped flow\" above phrases the delegated apply as acting on \"affected endpoints\" but underspecified two invariants; the field bug **F015B / BUILD-F015B-APPLYMATCH** (a live update to a `--adapter cc:ccs` PROFILE-COMPOSITE endpoint silently no-oped — `LIVE`/`DONE` printed, version-of-truth stayed OLD) exposed both. Recorded here as binding, guarded by **REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP** (KNOWN-HAZARDS 7.24):\r\n\r\n**Invariant A — ONE parent-aware matcher across the live-update seam.** A `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` on its session / perch `info.adapter`, while an update carries the PARENT record name `cc`. Every live-update comparator — the CLI live-gate (`adapter_has_live_endpoint`), the broker apply's session filter (`dispatch_adapter_apply`), and the endpoint-selection (`select_endpoints_running_adapter`) — MUST match on the parent through the single shared `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option(session_adapter).0 == parent`). An EXACT `==` against a record name at ANY of these seams silently drops every `:profile` endpoint (→ `affected=[]`). The original build had the broker t…\n\r\n**Invariant B — the daemon owns the whole apply; never report success without swapping.** Once the CLI delegates the apply to the broker there is NO CLI-side fallback swap. The broker's CRC swap therefore runs UNCONDITIONALLY — the terminate/restart loops simply no-op when nothing is resident — and `KIND_APPLIED` is reported ONLY after a real swap. The original `affected.is_empty() → reply KIND_APPLIED + return` branch (an optimization: \"the endpoint just exited, nothing to coordinate\") was the amplifier that made Invariant A's skew a SILENT no-op; it is removed. A genuinely-exited endpoint still swaps the on-disk install dir (correct — the next spawn must read new bytes).\r\n\r\n## Amendment 3 (F-030 psyche-ephemeral, 2026-07-04) — the resident-Psyche premise is retired; `.live-bin` own-copy dissolved\r\n\r\n<!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] -->\r\n\r\nThe **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:\r\n\r\n- **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 supervise, poll, or reap. This makes the Psyche's install-dir image lock **turn-bounded**, the *same accepted class as `[digest]`/`translate`/`[session.*]` today* — so the Psyche joins the **ephemeral** side of §\"Resident vs ephemeral\" outright, and Correction 1's \"excl…\n- **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 Correction 3's conclusion stands; only the Psyche's own-copy mechanism retires.\r\n- **Residency / thrash / reap machinery retired.** The `confirm_residency_or_unhost` residency arm (and the per-reconcile orphan reap of a resident Psyche, `REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP`) are removed with the resident child. A **one-shot legacy-resident sweep at brain start** replaces the ongoing reap — it kills only a pre-upgrade *stranded* resident wrapper (positive cmdline+basename match, fail-safe-decline, infra never-kill) and clears its stale `<parent>-psyche` ready registration; it is deliberately not per-reconcile/periodic (burying the residency era, not resurrecting it).\r\n- **A Psyche failure never touches the parent's hosted state.** Consecutive turn failures consume a bounded budget → a `psyche_host_error` stamp on the Self perch, never a teardown of the parent endpoint (KNOWN-HAZARDS 7.30/7.31, `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` / `REQ-HAZARD-THRASH-GUARD-BLIND`).\r\n\r\nNet: the daemon-coordinated apply is unchanged in shape — it still stops/CRC-swaps/restarts the **resident translation binary** — but the Psyche is no longer a coordination subject (nothing resident to stop), and the `.live-bin` relocation it once required is gone.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0025-live-daemon-coordinated-adapter-update.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195748345}}
{"type":"message","id":"0903a1a7","parentId":"fa9e6554","timestamp":"2026-07-16T09:55:48.365Z","message":{"role":"toolResult","toolCallId":"call_e0Xv9opRBBkjRUw9pm2LE9yB|fc_0cc7a0c9a797f232016a58aaa44bc4819a9d91bc739d9e10fc","toolName":"glob","content":[{"type":"text","text":"# crates/\n## spt/\n### 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\n#### fixtures/\ngit_fixture.rs\ngh_fixture.rs\n### src/\ncli.rs\nroster.rs\nrc.rs\nwansend.rs\n#### picker/\ndata.rs\nmod.rs\nview.rs\nmodel.rs\n#### api/\nreporting.rs\nstartup.rs\nauth.rs\nmod.rs\nworker.rs\ndelivery.rs\n## spt-store/\n### src/\nprojindex.rs\nprojderive.rs\nprojinval.rs\nlib.rs\nbranchstore.rs\ncontextstore.rs\ngitrun.rs\npsyche_custody.rs\nspool.rs\npeeraddrs.rs\nworker_reap.rs\nworker_seq.rs\ninfo.rs\nsessions.rs\nxfer.rs\nvisibility.rs\ntimefmt.rs\nsyncmerge.rs\nsubnet.rs\nshellinfo.rs\nseed.rs\nrotation.rs\nroster.rs\nrename.rs\nregistry.rs\nrecent_home.rs\nproject.rs\nproc.rs\nperch.rs\nnotif.rs\nnodeid.rs\nliveness.rs\nhostlabel.rs\nhome.rs\nhistory.rs\ngrants.rs\nepoch.rs\ndb.rs\ncontextmark.rs\nattachment.rs\natomic.rs\naccess.rs\n### tests/\ncarrier_claim_int.rs\n## spt-daemon/\n### src/\nsync.rs\nprojwriter.rs\nlib.rs\nbrainproc.rs\napplyhost.rs\ndocshost.rs\ndaemon.rs\nconfig.rs\nupdate.rs\nrelcache.rs\nrelease.rs\nnotif.rs\nrelay.rs\npropagate.rs\nmsg.rs\nnethost.rs\nlivehost.rs\ninject.rs\nconn.rs\nbroker.rs\nbrain.rs\nautostart.rs\nstderrlog.rs\nlifecycle.rs\ndigest.rs\npairhost.rs\nxfer.rs\nshellwake.rs\nshellchan.rs\nseedmap.rs\nresting.rs\nresthost.rs\nregistryhost.rs\nlinkhost.rs\neffect.rs\nattach.rs\nadapter_update.rs\n#### pump/\nmod.rs\n#### bin/\nxlate_choreo_fixture.rs\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\n## spt-msg/src/\nready.rs\ndeliver.rs\n## xtask/src/\nmain.rs\n## spt-runtime/src/\nmanifest.rs\nruntime.rs\nresolve.rs\nlib.rs\nregistry.rs\nprofile.rs\n## spt-live/src/\npulse.rs\ningest.rs\necho.rs\n## spt-net/src/\nnet.rs\n### net/\nregistry.rs\nxfer.rs\nwanmsg.rs\nupdate.rs\nsync.rs\nshelllink.rs\nserveprobe.rs\nrest.rs\n#### pairing/\nntp.rs\n## spt-term/\n### src/\nscreen.rs\nwinprog.rs\nsurface.rs\nstream.rs\nreader.rs\npty.rs\nprojection.rs\nlib.rs\ndigest.rs\n### tests/\nwinspawn.rs\nsurface.rs\nstream.rs\ninject.rs\ndsr.rs\n## spt-proto/src/\nversion.rs\npayload.rs\nlib.rs\nidentity.rs\nid.rs\nevent.rs\nenvelope.rs\nendpoint.rs\nchunk.rs\naddr.rs\n\n[200 results limit reached. Use limit=400 for more]"}],"details":{"scopePath":"crates/**/*.rs, tests/**/*.rs","fileCount":200,"files":["crates/spt/tests/projindex_reader_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/src/picker/data.rs","crates/spt/src/cli.rs","crates/spt/src/api/reporting.rs","crates/spt-store/src/projindex.rs","crates/spt-store/src/projderive.rs","crates/spt-daemon/src/sync.rs","crates/spt-daemon/src/projwriter.rs","crates/spt/tests/projindex_writer_e2e.rs","crates/spt/src/api/startup.rs","crates/spt-store/src/projinval.rs","crates/spt-store/src/lib.rs","crates/spt-store/src/branchstore.rs","crates/spt-store/src/contextstore.rs","crates/spt-msg/src/ready.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/brainproc.rs","crates/spt/tests/json_emit.rs","crates/spt/tests/whoami_identity_e2e.rs","crates/spt/src/roster.rs","crates/spt/tests/fixtures/git_fixture.rs","crates/spt-store/src/gitrun.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/fixtures/gh_fixture.rs","crates/spt/tests/composite_e2e.rs","crates/spt-daemon/src/applyhost.rs","crates/xtask/src/main.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/config.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt-runtime/src/manifest.rs","crates/spt-daemon/tests/propagate.rs","crates/spt-daemon/src/update.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/release.rs","crates/spt/tests/release_verify_e2e.rs","crates/spt/tests/bootstrap_verb_e2e.rs","crates/spt-daemon/src/notif.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/src/picker/mod.rs","crates/spt/src/api/auth.rs","crates/spt/src/api/mod.rs","crates/spt-store/tests/carrier_claim_int.rs","crates/spt-store/src/psyche_custody.rs","crates/spt-store/src/spool.rs","crates/spt-store/src/peeraddrs.rs","crates/spt-runtime/src/runtime.rs","crates/spt-runtime/src/resolve.rs","crates/spt-runtime/src/lib.rs","crates/spt-live/src/pulse.rs","crates/spt-msg/src/deliver.rs","crates/spt-live/src/ingest.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/src/relay.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt/tests/n1_pairing.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/src/rc.rs","crates/spt/src/api/worker.rs","crates/spt/src/api/delivery.rs","crates/spt-live/src/echo.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-daemon/src/stderrlog.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-net/src/net/pairing/ntp.rs","crates/spt-daemon/src/pairhost.rs","crates/spt/tests/worker_visibility_e2e.rs","crates/spt/tests/worker_lifecycle_e2e.rs","crates/spt-store/src/worker_reap.rs","crates/spt-store/src/worker_seq.rs","crates/spt-store/src/info.rs","crates/spt-runtime/src/registry.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/src/wansend.rs","crates/spt/src/picker/view.rs","crates/spt/src/picker/model.rs","crates/spt-term/src/screen.rs","crates/spt-store/src/sessions.rs","crates/spt-net/src/net/registry.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-daemon/src/xfer.rs","crates/spt-daemon/src/shellwake.rs","crates/spt-daemon/src/shellchan.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/resting.rs","crates/spt-daemon/src/resthost.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/linkhost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/adapter_update.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-term/src/winprog.rs","crates/spt-term/src/surface.rs","crates/spt-term/src/stream.rs","crates/spt-term/src/reader.rs","crates/spt-term/src/pty.rs","crates/spt-term/src/projection.rs","crates/spt-term/src/lib.rs","crates/spt-term/src/digest.rs","crates/spt-store/src/xfer.rs","crates/spt-store/src/visibility.rs","crates/spt-store/src/timefmt.rs","crates/spt-store/src/syncmerge.rs","crates/spt-store/src/subnet.rs","crates/spt-store/src/shellinfo.rs","crates/spt-store/src/seed.rs","crates/spt-store/src/rotation.rs","crates/spt-store/src/roster.rs","crates/spt-store/src/rename.rs","crates/spt-store/src/registry.rs","crates/spt-store/src/recent_home.rs","crates/spt-store/src/project.rs","crates/spt-store/src/proc.rs","crates/spt-store/src/perch.rs","crates/spt-store/src/notif.rs","crates/spt-store/src/nodeid.rs","crates/spt-store/src/liveness.rs","crates/spt-store/src/hostlabel.rs","crates/spt-store/src/home.rs","crates/spt-store/src/history.rs","crates/spt-store/src/grants.rs","crates/spt-store/src/epoch.rs","crates/spt-store/src/db.rs","crates/spt-store/src/contextmark.rs","crates/spt-store/src/attachment.rs","crates/spt-store/src/atomic.rs","crates/spt-store/src/access.rs","crates/spt-runtime/src/profile.rs","crates/spt-proto/src/version.rs","crates/spt-proto/src/payload.rs","crates/spt-proto/src/lib.rs","crates/spt-proto/src/identity.rs","crates/spt-proto/src/id.rs","crates/spt-proto/src/event.rs","crates/spt-proto/src/envelope.rs","crates/spt-proto/src/endpoint.rs","crates/spt-proto/src/chunk.rs","crates/spt-proto/src/addr.rs","crates/spt-net/src/net.rs","crates/spt-net/src/net/xfer.rs","crates/spt-net/src/net/wanmsg.rs","crates/spt-net/src/net/update.rs","crates/spt-net/src/net/sync.rs","crates/spt-net/src/net/shelllink.rs","crates/spt-net/src/net/serveprobe.rs","crates/spt-net/src/net/rest.rs"],"truncated":true,"resultLimitReached":200,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","meta":{"limits":{"resultLimit":{"reached":200,"suggestion":400}}}},"isError":false,"timestamp":1784195748364}}
{"type":"message","id":"6abbe046","parentId":"0903a1a7","timestamp":"2026-07-16T09:55:48.380Z","message":{"role":"toolResult","toolCallId":"call_4fxqz3VVDTNQlqYRkgrV559Z|fc_0cc7a0c9a797f232016a58aaa44be4819aab826dc23c9504a8","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n## Cargo.toml\n 5|publish = false\n*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) — the `spt` binary is a thin CLI over it. B0 lands the crate + the version...\n 7|\n 8|[dependencies]\n 9|# Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker\n\n## src/\n### applyhost.rs\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 33|//!    broker holds them all across the cycle). This replaces the old in-process\n*34|//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n 35|//!    subscriber while the live brain kept running the OLD code.\n 36|//!\n*37|//! New code runs **immediately** — the supervised respawn lands on the new\n 38|//! executable, no manual bounce, no logon. Readiness-gated promotion +\n 39|//! auto-rollback on a brain that fails to come up is D6.\n 40|// [impl->REQ-UPD-3]\n...\n 77|    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n*78|    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n 79|    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n 80|    /// provisional immediately (it cannot observe the asynchronous verdict — the\n 81|    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n...\n 122|/// the brain-only class gate. Pure w.r.t. the daemon — reads only the cache +\n*123|/// keys, swaps nothing. Both the broker-handoff apply and the daemonless apply\n 124|/// run this identical gate so their fail-closed posture never diverges.\n 125|// [impl->REQ-UPD-4]\n 126|fn prepare_apply(cache: &ReleaseCache, release_keys: &Path) -> Result<Prepared, String> {\n...\n 248|/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n*249|/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n 250|/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n*251|/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n 252|/// whether a running brain is cycled afterwards.\n 253|// [impl->REQ-UPD-6]\n 254|fn swap_and_record(\n...\n 273|    // carrying the last-known-good `rollback_binary` (the aside we just created)\n*274|    // so the broker's supervisor, a different process from this CLI, can promote\n 275|    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n 276|    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n 277|    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n*278|    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n 279|    // [impl->REQ-UPD-6]\n 280|    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n 281|        version,\n...\n 284|        // The version actually running before this swap — what a rollback returns\n*285|        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n 286|        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n 287|        // the quarantined version, not the running one. `None` when nothing was\n*288|        // ever recorded applied (a first-ever apply) → the supervisor falls back\n 289|        // to N-1, the only honest guess then.\n 290|        prior_version: cache.applied_version(),\n 291|    });\n...\n 297|/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n*298|/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299|/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n 301|// [impl->REQ-UPD-3]\n...\n 327|    // REQ-UPD-3's moment (ADR-0018 D3-3): trigger a real brain-PROCESS restart.\n*328|    // The broker signals its supervisor to cycle the brain child onto the\n 329|    // just-swapped binary with `start-reason=update`; new code runs immediately,\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 335|    //\n*336|    // A broker without a supervisor wired (`honored: false`) still swapped the\n 337|    // binary — the next daemon start uses the new executable. Readiness-gated\n 338|    // promotion + auto-rollback on a brain that fails to come up is D6.\n 339|    match brain.request_brain_restart() {\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...\n 360|/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n*361|/// mirrors the apply path: `false` means the broker has no supervisor wired\n 362|/// (accepted, not acted on).\n 363|// [impl->REQ-DAEMON-REFRESH]\n*364|pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365|    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366|    brain\n 367|        .request_brain_restart()\n...\n 379|/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n*380|/// the handoff path's does. Runs the identical verify/classify gate as\n 381|/// [`apply_staged`], so every fail-closed refusal is preserved.\n 382|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n 383|// [impl->REQ-UPD-4]\n### brain.rs\n 8|//!\n*9|//! - **`gen_start = now()` on cold-start *and* handoff (KNOWN-HAZARDS 2.4).**\n 10|//!   The wall-clock generation stamp is taken fresh on every start. A handed-off\n 11|//!   brain rehydrates its resume cursor and generation *counter* from prior\n 12|//!   state, but its `gen_start` is `now()`, **never** the stale value from that\n 13|//!   state — a rehydrated `gen_start` would fire time-based discriminators on\n*14|//!   the new generation. [`Brain::handoff`] takes the prior state by value yet\n 15|//!   ignores its `gen_start_ms`.\n 16|//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next\n*17|//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;\n 18|//!   the broker replays buffered output from there. [`Brain::read_event`] accepts\n 19|//!   only the contiguous next `seq` (advancing the cursor), silently drops an\n 20|//!   already-seen `seq` (a replay duplicate), and treats a forward gap as a hard\n...\n 68|\n*69|/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n 70|/// advance the generation. `gen_start_ms` is recorded for observability but is\n*71|/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n 72|/// fresh `gen_start` from `now()` (2.4).\n 73|///\n 74|/// **Test-only continuity (restoration D4-3, ADR-0018 Q6).** This brain→brain\n*75|/// frame is **retired from the production path**: a brain the broker's supervisor\n 76|/// respawns reconstructs continuity by querying the broker\n 77|/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n*78|/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n 79|/// solely because the integration tests (separate test target — `#[cfg(test)]`\n*80|/// would hide them) still drive the handoff-frame shape directly; there is **no\n 81|/// non-test caller** (KNOWN-HAZARDS 2.4 grep-clean close-out).\n 82|#[derive(Debug, Clone)]\n 83|pub struct BrainState {\n...\n 85|    pub session_id: u64,\n*86|    /// The generation counter (incremented on each handoff).\n 87|    pub generation: u64,\n 88|    /// The next output sequence still needed (the resume cursor).\n 89|    pub next_seq: u64,\n 90|    /// The prior generation's wall-clock start (ignored as authoritative on\n*91|    /// handoff — kept only to demonstrate it is *not* carried forward).\n 92|    pub gen_start_ms: u64,\n 93|}\n 94|\n...\n 204|/// channel supplies the deadline. On a timeout the pump bubbles out to a\n*205|/// supervised restart (a fresh [`Brain`] → fresh split → fresh reader thread);\n 206|/// the old reader, parked in `read_frame`, is abandoned (KH 7.6 leak watch — the\n 207|/// old conn's drop eventually errors its read and the thread exits).\n 208|enum BrainConn {\n...\n 216|        /// The reader-thread handle — held so the thread is owned, not detached\n*217|        /// silently. On a supervised restart the whole [`Brain`] drops; this\n 218|        /// handle drops with it (the thread, blocked in `read_frame`, is\n 219|        /// abandoned and exits when the old conn closes — KH 7.6).\n 220|        _reader: JoinHandle<()>,\n...\n 272|    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.\n*273|    /// Durability across a handoff is the logic layer's job (a re-attaching\n 274|    /// brain queries `net-streams` and re-subscribes from its durable cursors).\n 275|    net_cursors: std::collections::HashMap<u64, u64>,\n 276|    /// The presence resume cursor (D4c) — the conn-liveness sibling of\n...\n 283|    /// [`Brain::resume_sessions`], replacing the retired single-session\n*284|    /// `BrainState` handoff. A session present here is in **resume mode** — its\n 285|    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n 286|    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n 287|    /// boundary re-send) and accept-and-snap `seq >= cursor` (the contiguous case\n...\n 367|    ///\n*368|    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n*369|    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n 370|    /// integration tests) replays output through it with no later `attach`. So it\n 371|    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n 372|    /// ALSO subscribes from 0 over this same socket → two racing\n...\n 381|    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n*382|    /// never this brain→brain frame; `handoff` survives only for the integration\n 383|    /// tests. See [`BrainState`].\n 384|    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n*385|    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n 386|        let mut conn = LocalSocketTransport::connect(name)?;\n 387|        send_hello(&mut conn, Role::Brain)?;\n 388|        // DEFENSE-IN-DEPTH (P1c): seed the dedup cursor at the resume point so\n...\n 415|\n*416|    /// The generation counter (0 on cold start, +1 per handoff).\n 417|    pub fn generation(&self) -> u64 {\n 418|        self.generation\n 419|    }\n...\n 430|\n*431|    /// Snapshot durable state for a future handoff. **Test-only (restoration\n 432|    /// D4-3)** — the production path reconstructs from the broker, never a\n 433|    /// snapshot frame; see [`BrainState`].\n 434|    pub fn snapshot(&self) -> BrainState {\n### brainproc.rs\n*1|//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n 2|//! broker/brain process-isolation restoration, D1 skeleton).\n 3|//!\n 4|//! ADR-0004 specified the broker/brain split as two **processes**; production\n...\n 7|//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n*8|//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n 9|//! `spt daemon brain` child. The child connects over the existing broker socket\n 10|//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n 11|//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n...\n 18|//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n*19|//! broker spawns + supervises a brain child over the socket, and the broker\n 20|//! (with its PTY children + seed channel) survives the brain dying — the\n*21|//! supervisor respawns it from the executable path, which on a real update is\n 22|//! the swapped binary.\n 23|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 24|\n...\n 37|/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n*38|/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n 39|/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\n 40|const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n 41|/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n...\n 51|/// (a transient first-boot hiccup should not abandon a good binary) before the\n*52|/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n 53|/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n 54|/// window-expiry triggers rollback.\n 55|const TRIAL_PREREADY_BUDGET: u32 = 3;\n...\n 61|const BRAIN_CONNECT_TRIES: u32 = 300;\n*62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n 63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n 64|\n 65|/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n...\n 74|#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n*75|pub enum StartReason {\n 76|    /// The broker's first brain spawn this broker lifetime.\n 77|    Cold,\n*78|    /// A supervised respawn after an unexpected brain exit.\n 79|    Crash,\n 80|    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n 81|    Update,\n...\n 83|\n*84|impl StartReason {\n 85|    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n 86|    pub fn as_arg(self) -> &'static str {\n 87|        match self {\n*88|            StartReason::Cold => \"cold\",\n*89|            StartReason::Crash => \"crash\",\n*90|            StartReason::Update => \"update\",\n 91|        }\n 92|    }\n 93|\n...\n 98|        match s {\n*99|            \"update\" => StartReason::Update,\n*100|            \"crash\" => StartReason::Crash,\n*101|            _ => StartReason::Cold,\n 102|        }\n 103|    }\n 104|}\n 105|\n*106|/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n*107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n 108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n*110|/// between the broker serve path and the supervisor — both run in the broker\n 111|/// process. This is the seam that makes a routine `apply` actually run new code,\n 112|/// the pillar the in-process-collapse regression silently unrealized.\n 113|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n...\n 130|\n*131|    /// Consume a pending request (supervisor side): true at most once per\n 132|    /// `request`, clearing it so the next cycle starts fresh.\n 133|    pub fn take(&self) -> bool {\n 134|        self.requested.swap(false, Ordering::SeqCst)\n### broker.rs\n 9|//!\n*10|//! ## Gapless handoff (B2 — the load-bearing invariant)\n 11|//!\n 12|//! A brain may be killed and restarted at any moment (a routine self-update, a\n 13|//! crash). The broker — and every hosted child — must survive that untouched,\n...\n 145|\n*146|/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n 147|/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n 148|/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n 149|/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n...\n 335|/// shared with the inject worker and RESET to 0 on a healthy `{commit}`, so a\n*336|/// respawned binary that recovers refreshes the whole budget (the C3(b) give-up-\n 337|/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n 338|/// psyche crash-loop backoff's bounded give-up.\n 339|const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n...\n 502|    by: Option<String>,\n*503|    /// Time of the last SUCCESSFUL handoff (`try_send` Ok) to this controller's\n 504|    /// writer channel (b4, REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE).\n 505|    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n 506|    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n...\n 516|    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n*517|    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n 518|    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n 519|    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n 520|    /// `last_ok` stale. `Arc<Mutex<…>>` so the reader observes the writer's live\n...\n 726|    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n*727|    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n 728|    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n 729|    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n 730|    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n*731|    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n 732|    /// (bounded wedge preserved). The writer still does the blocking socket write +\n 733|    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n 734|    /// single drain thread.\n...\n 824|        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n*825|        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n 826|        // slow controller falls behind the ring) unless it has been Full for the\n 827|        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n 828|        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n...\n 1507|// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n*1508|#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n 1509|fn controller_writer(\n 1510|    sid: u64,\n 1511|    send: SharedSend,\n...\n 1603|            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n*1604|            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n 1605|            // a seq (dropped while Full). The frame still went to the socket (the live\n 1606|            // operator sees it), but the durable resume cursor must NOT jump past the\n 1607|            // gap — see [`contiguous_advance`].\n...\n 1678|    /// increments it per rebuild, the worker RESETS it to 0 on a healthy `{commit}`\n*1679|    /// (so a recovered binary refreshes the whole budget). At `translate_respawn_\n 1680|    /// budget()` the daemon gives up (loud) and leaves the binary dead → spool.\n 1681|    translation_respawns: Arc<AtomicU32>,\n 1682|    /// The adapter name owning this session (W3a, ADR-0025) — empty for a\n...\n 1948|            thread::spawn(move || {\n*1949|                run_inject_worker_supervised(\n 1950|                    sup_child,\n 1951|                    sup_faulted,\n 1952|                    sup_endpoint,\n...\n 2008|// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\n*2009|fn run_inject_worker_supervised(\n 2010|    child: Arc<TranslationChild>,\n 2011|    faulted: Arc<AtomicBool>,\n 2012|    endpoint: String,\n...\n 2375|        if committed {\n*2376|            // Healthy sequence: reset the strike counter, refresh the respawn budget\n 2377|            // (a working binary heals it), and forget any earlier respool of THIS\n 2378|            // envelope (it succeeded → a future miss may respool it afresh).\n 2379|            strikes = 0;\n...\n 2540|    net: std::sync::OnceLock<NetHost>,\n*2541|    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542|    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543|    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n*2544|    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n*2545|    /// `Daemon::run` after it spawns the supervisor.\n 2546|    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,\n 2547|    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a\n 2548|    /// `session_id → (subnet, real address, 5-min deadline)` between\n...\n 2703|\n*2704|    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n 2705|    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n*2706|    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707|    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n### config.rs\n 27|\n*28|/// Default orphan-watch grace window (B7): how long the supervised-crash teardown\n 29|/// waits after observing a hosted Self's harness owner gone before it rechecks\n 30|/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n*31|/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n 32|/// teardown for long. Configurable per machine via `crash_grace_ms`.\n 33|pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n 34|\n...\n 137|    /// The orphan-watch grace window (B7): the recovery wait before a\n*138|    /// supervised-crash teardown rechecks liveness and signs off (1.1).\n 139|    pub crash_grace: Duration,\n 140|    /// Whether self-update is full-auto (C2, REQ-UPD-4). **Default `false`** — an\n 141|    /// update is gated on user confirmation delivered to the most-recently-active\n### daemon.rs\n 185|        let net_up = net.is_some();\n*186|        // Keep the broker handle alive past the bind so the brain supervisor's\n 187|        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188|        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189|        let broker_handle: Option<Arc<Broker>> =\n 190|            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191|                Ok(broker) => {\n...\n 311|\n*312|        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n 313|        // restoration): the broker is the always-up per-machine anchor; it spawns\n*314|        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\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...\n 322|        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n*323|        // container the supervisor enrolls every (re)spawned brain into. Created\n*324|        // here so it outlives the supervisor and is reachable from the\n 325|        // graceful-stop path below. The brain subtree (the brain + its shellwake\n 326|        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n 327|        // never in the container.\n...\n 329|        let reaper = crate::reap::BrainReaper::new();\n*330|        // The supervisor stop flag — hoisted out of the wiring block (was held\n 331|        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n*332|        // it, so the supervisor does not respawn the brain we are about to reap.\n 333|        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n 334|        {\n*335|            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n 336|                Arc::clone(&brain_stop),\n 337|                Arc::clone(&reaper),\n 338|                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n...\n 358|        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n*359|        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n 360|        // does not respawn the brain we are about to kill) THEN reap the brain\n 361|        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n 362|        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n...\n 371|    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n*372|    /// broker process spawns + supervises this; it connects to the broker over\n 373|    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n 374|    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n 375|    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n*376|    /// is parsed leniently ([`crate::brainproc::StartReason::from_arg`]) so an\n 377|    /// unknown value from a newer broker defaults to `Cold` (KH-2.3).\n 378|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 379|    pub fn run_brain(generation: u64, start_reason: &str) -> io::Result<()> {\n...\n 390|            generation,\n*391|            crate::brainproc::StartReason::from_arg(start_reason),\n 392|        )\n 393|    }\n 394|}\n...\n 430|\n*431|/// Background net self-heal supervisor (REQ-DAEMON-9): a daemon that came up\n 432|/// net-less (NET_BIND_FAIL at boot — typically the autostart racing the network\n 433|/// up) keeps retrying `try_start_net` with capped backoff and, on success,\n 434|/// ATTACHES the host to the live broker (`OnceLock`, so exactly once). The node\n### deadline.rs\n 3|//! Phase-significant periodic timing (the pulse loop) and one-shot deadlines\n*4|//! (alarms) must survive a brain swap: the supervisor respawns a fresh brain\n 5|//! (`Brain::cold_start`) and the outgoing brain is gone before the new one starts\n 6|//! (D3-3), so continuity cannot ride a brain→brain frame — it must come from the\n 7|//! persistent side (disk), exactly as D4 moved session continuity to the broker.\n...\n 13|//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n*14|//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n*15|//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n 16|//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n*17|//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n 18|//!   IPC round-trip.\n 19|//!\n 20|//! - [`OneShotDeadline`] — an **absolute** `target_ms` fired once. Persisted at\n 21|//!   creation; **every** start reads it and fires-if-due; it is **never reset on\n*22|//!   any** `StartReason` (\"remind me at 3pm\" is a commitment that outlives any\n 23|//!   restart — the asymmetry vs the periodic crash-reset IS the rule). Built as a\n 24|//!   tested-but-unwired **pure helper** (ADR-0018 V3): the daemon has no one-shot\n 25|//!   consumer today (alarm is a legacy-listener in-memory timer,\n...\n 38|\n*39|use crate::brainproc::StartReason;\n 40|use crate::config::DEFAULT_PULSE_PERIOD;\n 41|\n 42|/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n...\n 77|\n*78|    /// Load-or-initialize the anchor for `key`, applying the [`StartReason`]\n 79|    /// rehydration rule (ADR-0018 Q4):\n 80|    ///\n*81|    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n*82|    ///   only the interval refreshes from live config. If no file exists (updating\n 83|    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n 84|    ///   — never fail an update on a missing file.\n*85|    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n 86|    ///   at `now_ms` (phase reset acceptable; cold is the first-ever start).\n 87|    ///\n 88|    /// A corrupt file is treated as absent (degrade-to-fresh), matching\n...\n 94|        interval_ms: u64,\n*95|        reason: StartReason,\n 96|        now_ms: u64,\n 97|    ) -> std::io::Result<Self> {\n 98|        let path = anchor_path(key);\n...\n 100|        let anchor = match (reason, Self::load(&path)) {\n*101|            // Update keeps the persisted phase; the interval refreshes to config.\n*102|            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103|                anchor_ms: prev.anchor_ms,\n 104|                interval_ms,\n 105|            },\n...\n 180|/// **rule** as a tested-but-unwired pure helper: persist `target_ms` at creation,\n*181|/// fire-if-due on every start, **never reset** on any [`StartReason`]. No\n 182|/// scheduler/timer/thread lives here; the durable in-daemon alarm scheduler that\n 183|/// will drive it is the alarm-port deferral (`docs/DEFERRED.md`). [`fire_if_due`]\n*184|/// takes **no** `StartReason` — that absence IS the never-reset rule, made\n 185|/// explicit in code so the alarm port cannot re-derive the crash-asymmetry wrong.\n 186|///\n 187|/// [`fire_if_due`]: OneShotDeadline::fire_if_due\n...\n 303|        with_home(|_| {\n*304|            let cold = DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 305|            assert_eq!(cold.anchor_ms, 1_000);\n 306|            // A crash start rewrites the anchor to the restart instant (phase reset).\n*307|            let crash = DeadlineAnchor::open(\"pulse\", 100, StartReason::Crash, 9_999).unwrap();\n 308|            assert_eq!(crash.anchor_ms, 9_999);\n 309|        });\n 310|    }\n...\n 312|    #[test] // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*313|    fn update_keeps_anchor_and_refreshes_interval() {\n 314|        with_home(|_| {\n*315|            DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 316|            // Update preserves phase (anchor) but the interval tracks live config.\n*317|            let upd = DeadlineAnchor::open(\"pulse\", 250, StartReason::Update, 9_999).unwrap();\n 318|            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n### digest.rs\n 117|/// `from` points at the changed tail (a minimal delta); a window slide evicts the\n*118|/// oldest turn → the heads differ from index 0 → `from == 0` (a full refresh).\n 119|pub fn common_prefix_len(a: &[Turn], b: &[Turn]) -> usize {\n 120|    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()\n 121|}\n...\n 531|    // [unit->REQ-TERM-4] the diff base: a shared head yields its length (a tail\n*532|    // delta); a divergent front yields 0 (a full refresh).\n 533|    #[test]\n 534|    fn common_prefix_len_finds_the_unchanged_head() {\n 535|        let a = vec![turn(\"a\"), turn(\"b\")];\n### digesthub.rs\n 121|        // The changed turns begin past the equal-by-value head; a window slide\n*122|        // (front eviction) diverges at 0 → a full refresh.\n 123|        let from = common_prefix_len(&st.snapshot.turns, &new.turns);\n 124|        st.version += 1;\n 125|        let update = DigestUpdate {\n...\n 463|    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a\n*464|    // tail delta (from past the unchanged head); a window slide refreshes from 0.\n 465|    #[test]\n*466|    fn publish_delta_is_tail_then_full_refresh() {\n 467|        let hub = DigestHub::new();\n 468|        let (_b, _v, rx) = hub.subscribe(\"doyle\");\n 469|        hub.project_and_publish(\"doyle\", digest(vec![turn(\"a\"), turn(\"b\")]));\n...\n 477|        assert_eq!(up.turns[0].input.as_deref(), Some(\"c\"));\n*478|        // A front eviction (window slide) diverges at 0 → full refresh.\n 479|        hub.project_and_publish(\"doyle\", digest(vec![turn(\"b\"), turn(\"c\"), turn(\"d\")]));\n 480|        let up = rx.recv().unwrap();\n 481|        assert_eq!(up.from, 0, \"front eviction resends from 0\");\n### endpoint.rs\n 35|\n*36|/// The broker listener name (B1/B2): PTY hosting + gapless handoff.\n 37|pub fn broker_socket_name() -> String {\n 38|    format!(\"spt-daemon-broker-{}.sock\", home_tag())\n 39|}\n...\n 75|/// child's pid, rewritten each heartbeat). The broker spawns the brain as a\n*76|/// supervised child process (ADR-0018 Q2/Q3 — the two-process restoration); the\n*77|/// child writes this once it has connected to the broker socket, and refreshes\n 78|/// it while attached. A test (or a future readiness-gated rollback, ADR-0018\n 79|/// Q7) reads it to confirm the brain came up and — on a pid change — that it\n 80|/// respawned. Best-effort like the daemon pid breadcrumb; liveness of the\n### lib.rs\n*1|//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).\n 2|//!\n 3|//! ADR-0004 splits the logical daemon into two implementation layers so a\n 4|//! routine self-update swaps the logic without disturbing live endpoints:\n...\n 45|//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).\n*46|//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →\n 47|//! drop → re-attach — so the live PTY child + its output stream survive the logic\n*48|//! swap untouched; it never re-implements the handoff and never touches a broker\n 49|//! resource.\n 50|//!\n*51|//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a\n 52|//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback\n 53|//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key\n 54|//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a\n...\n 97|//!\n*98|//! The brain + gapless handoff (B2) and the rest of the M3b task table build on\n*99|//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK\n 100|//! (R-ARCH-2).\n 101|//!\n 102|//! [`Envelope`]: frame::Envelope\n...\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### lifecycle.rs\n 35|\n*36|use crate::brainproc::StartReason;\n 37|use crate::config::DaemonConfig;\n 38|use crate::deadline::DeadlineAnchor;\n 39|use crate::notif::NotifSurfacePolicy;\n...\n 307|/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n*308|/// **refresh** them while the brain driver thread keeps running. The\n 309|/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n 310|/// otherwise unreachable from an IPC apply handler — so ADR-0025's \"re-clone the\n 311|/// manifest into the running BrainLifecycle\" is realized as a clonable handle the\n...\n 331|    /// never holds the read lock across the spawn/blocking call — a concurrent\n*332|    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n 333|    fn runtime_snapshot(&self) -> ManifestRuntime {\n 334|        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n 335|    }\n...\n 342|    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n*343|    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n 344|    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n 345|    /// brain thread, PTY, broker session, and output log are untouched; only these\n 346|    /// two cells' contents change. (W3c)\n 347|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n*348|    pub fn refresh(&self, new: Manifest, install_dir: Option<&Path>) {\n 349|        let runtime = build_runtime(&new, install_dir);\n 350|        // Write the runtime first, then the manifest: a reader that snapshots\n 351|        // between the two writes sees the new runtime with the old manifest at\n...\n 371|pub struct BrainLifecycle {\n*372|    /// Manifest + runtime behind a refreshable shared handle (W3c).\n 373|    cell: ManifestCell,\n 374|    /// The adapter install dir, retained for the W3c reload-on-dirty poll\n*375|    /// (`<install_dir>/manifest.toml`) and to re-pin the runtime on refresh.\n 376|    /// `None` ⇒ PATH-only resolution + no live-reload (a test/legacy host).\n 377|    install_dir: Option<PathBuf>,\n 378|    id: String,\n...\n 437|        // psyche role's bare program token resolves there before PATH (a\n*438|        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n 439|        Some(BrainLifecycle {\n 440|            cell: ManifestCell::new(manifest.clone(), install_dir),\n 441|            install_dir: install_dir.map(Path::to_path_buf),\n...\n 450|\n*451|    /// Refresh this endpoint's manifest + runtime in place — a thin delegate to\n 452|    /// the shared cell. The production trigger is [`reload_manifest_if_changed`]\n 453|    /// (the brain pulling an on-disk swap); this direct entry exists for tests and\n 454|    /// any in-process caller. Brain-parity: nothing else cycles. (W3c)\n 455|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n*456|    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {\n*457|        self.cell.refresh(new, install_dir);\n 458|    }\n 459|\n 460|    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\n...\n 470|    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\n*471|    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\n 472|    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\n 473|    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\n 474|    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\n...\n 498|            Some(new) => {\n*499|                self.cell.refresh(new, Some(dir));\n 500|                *last = Some(hash);\n 501|            }\n 502|            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\n...\n 581|\n*582|    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n 583|    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n 584|    /// echo-commune (T3).\n 585|    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n...\n 587|        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n*588|        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n 589|        // `last_active_ms` — a resting seat ticking its pulse must never win\n 590|        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n 591|        // the notif first-fire). A dormant seat instead checks the opt-in\n...\n 682|    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\n*683|    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\n 684|    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\n 685|    /// deadline is derived functionally — **no per-fire write**.\n 686|    ///\n...\n 695|        stop: &AtomicBool,\n*696|        reason: StartReason,\n 697|        mut on_tick: F,\n 698|    ) {\n 699|        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\n...\n 714|        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n*715|        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n 716|        // the initial state.\n 717|        let mut manifest_hash = self.manifest_disk_hash();\n 718|        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n...\n 755|        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n*756|        // history fetch below), so a live-update refresh never waits on it.\n 757|        let manifest = self.cell.manifest_snapshot();\n 758|        let Some(h) = &manifest.history else {\n 759|            return Vec::new();\n...\n 1213|    /// the Self is leaving on purpose, there is nothing to recover. The\n*1214|    /// supervised-crash path ([`supervised_crash_signoff`](Self::supervised_crash_signoff))\n 1215|    /// supplies the real grace + liveness recheck for an *un*intentional death.\n 1216|    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {\n 1217|        self.signoff_with(session_id, Duration::ZERO, || true)\n### livehost.rs\n 5|//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n*6|//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n 7|//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n 8|//! disk): same shape — enumerate Self perches, resolve each manifest from its\n*9|//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run\n 10|//! one bounded reconcile sweep at boot and every tick. Where the shell waker\n 11|//! hosts **offline** shells, this hosts **online** live endpoints — `status` is\n 12|//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the\n...\n 38|use crate::brain::{now_ms, Brain};\n*39|use crate::brainproc::StartReason;\n 40|use crate::config::DaemonConfig;\n 41|use crate::lifecycle::BrainLifecycle;\n 42|\n...\n 150|    cfg: &DaemonConfig,\n*151|    reason: StartReason,\n 152|) {\n 153|    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n 154|    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n...\n 613|/// daemon online-stamp makes liveness daemon-authoritative), then start the\n*614|/// config-paced pulse driver on its own supervised thread with a held stop flag.\n 615|/// A Psyche-spawn failure tables nothing — the next sweep retries.\n 616|// [impl->REQ-DAEMON-1]\n 617|fn host_one(\n...\n 621|    session_id: String,\n*622|    reason: StartReason,\n 623|) {\n 624|    lifecycle.sweep_on_start();\n 625|    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n...\n 977|// [impl->REQ-DAEMON-1]\n*978|pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n 979|    std::thread::spawn(move || {\n 980|        let set = Arc::new(LiveSet::new());\n 981|        {\n...\n 1316|                &cfg,\n*1317|                StartReason::Cold,\n 1318|            );\n 1319|\n 1320|            let status = |id: &str| {\n...\n 1391|                &fast_cfg(),\n*1392|                StartReason::Cold,\n 1393|            );\n 1394|\n 1395|            let exists = |wid: &str| {\n...\n 1573|                &cfg,\n*1574|                StartReason::Cold,\n 1575|            );\n 1576|            assert_eq!(set.len(), 1, \"the online live endpoint is hosted\");\n 1577|\n...\n 1608|                &fast_cfg(),\n*1609|                StartReason::Cold,\n 1610|            );\n 1611|            assert!(set.is_empty(), \"an offline live endpoint is never hosted\");\n 1612|        });\n...\n 1633|                    &cfg,\n*1634|                    StartReason::Cold,\n 1635|                )\n 1636|            };\n 1637|            run();\n...\n 1664|                &cfg,\n*1665|                StartReason::Cold,\n 1666|            );\n 1667|            assert_eq!(first.len(), 1);\n 1668|            // Brain \"restart\": drop the table (threads die with the process), the\n...\n 1680|                &cfg,\n*1681|                StartReason::Crash,\n 1682|            );\n 1683|            assert_eq!(second.len(), 1, \"a fresh brain re-hosts the online endpoint\");\n 1684|            set_then_teardown(&second);\n...\n 1719|            };\n*1720|            run(StartReason::Cold);\n 1721|            assert_eq!(set.len(), 1, \"hosted while online\");\n 1722|\n 1723|            // Graceful signoff flips the Self perch offline WHILE the driver pulses.\n...\n 1726|\n*1727|            run(StartReason::Cold);\n 1728|            assert!(\n 1729|                set.is_empty(),\n 1730|                \"an offline-transitioned endpoint is un-hosted (driver stopped + de-tabled)\"\n...\n 1755|                    &cfg,\n*1756|                    StartReason::Cold,\n 1757|                )\n 1758|            };\n 1759|            run();\n...\n 1991|                &fast_cfg(),\n*1992|                StartReason::Cold,\n 1993|            );\n 1994|            let perch = perch::resolve_perch_path(\"cold\", ParentHint::Infer);\n 1995|            let info = spt_store::info::read_info(&perch).unwrap();\n...\n 2028|                &fast_cfg(),\n*2029|                StartReason::Cold,\n 2030|            );\n 2031|            let info = spt_store::info::read_info(&perch).unwrap();\n 2032|            assert!(info.host_error.is_none(), \"no material → benign, not an error\");\n...\n 2052|                &fast_cfg(),\n*2053|                StartReason::Cold,\n 2054|            );\n 2055|            let info = spt_store::info::read_info(&perch).unwrap();\n 2056|            assert!(info.host_error.is_none(), \"not woken → no attempt, no error\");\n### msg.rs\n 226|/// Q3). `spt update apply` sends this after swapping the binary on disk; the\n*227|/// broker signals its brain supervisor to cycle the brain child with\n 228|/// `start-reason=update`, so the live brain restarts onto the new binary while\n 229|/// every broker-held endpoint stays untouched at the process level. Additive\n 230|/// verb (KH-2.3): an older broker that predates it answers with a generic error,\n...\n 233|/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n*234|/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235|/// acceptance, not completion. `honored` is false when the broker has no\n*236|/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237|/// reached a broker that cannot itself respawn.\n 238|pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239|\n...\n 872|/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n*873|/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874|/// cycle the child; false when the broker has none wired (a bare test broker),\n 875|/// so the caller can tell a real planned restart from a mere accept.\n 876|#[derive(Debug, Clone, Serialize, Deserialize)]\n### nethost.rs\n 68|/// continues, heartbeat advances), NOT the brain's own read-deadline (the A-half\n*69|/// poison → supervised-restart path). The 20s margin below 30s covers the error\n 70|/// frame's IPC transit + the brain's read scheduling. Test-overridable via\n 71|/// [`NetHost::set_quic_op_timeout`] so the hermetic black-hole tests fail fast.\n 72|const BROKER_QUIC_OP_TIMEOUT_MS: u64 = 10_000;\n...\n 988|    /// round continues, heartbeat advances) — DISTINCT from the brain's OWN\n*989|    /// read-deadline `TimedOut` (the A-half poison → supervised-restart signal).\n 990|    /// Because the bound fires (10s) well before the brain's 30s\n 991|    /// `PUMP_PEER_IO_TIMEOUT`, the broker always REPLIES first. A live op\n 992|    /// completes with zero added latency; the bound only bites a non-responsive\n### notif.rs\n 57|/// Producer kind for the auto-rollback alert (ADR-0018 Q7, restoration D6-2): a\n*58|/// brain-only update failed readiness and the supervisor rolled back to the\n 59|/// last-known-good binary. A distinct kind so a surface can style it; it rides\n 60|/// the same first-fire + **boundary-resurface** path as every other producer, so\n 61|/// an undismissed rollback row re-surfaces until the operator dismisses it\n...\n 426|/// Q7): a brain-only update `v{quarantine_version}` failed to reach `ready` and\n*427|/// the supervisor rolled back to `v{running_version}`, quarantining the bad\n 428|/// version. Reuses the one producer front door ([`produce_and_first_fire`]) so\n 429|/// the row rides first-fire AND **boundary-resurface** — an undismissed rollback\n 430|/// row re-surfaces at every boundary until the operator dismisses it (the\n 431|/// \"loud, not fire-once\" requirement; no new persistence path). The broker's\n*432|/// supervisor calls this once per member subnet on rollback.\n 433|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 434|#[allow(clippy::too_many_arguments)]\n 435|pub fn produce_rollback_notif(\n### pairhost.rs\n 76|    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);\n*77|    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every\n 78|    // served subnet so the joiner always learns at least us, then hand it the\n 79|    // whole current roster in the Seed frame.\n 80|    let mut roster = RosterStore::load();\n*81|    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());\n 82|    match tokio::time::timeout(\n 83|        CEREMONY_TIMEOUT,\n 84|        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),\n...\n 401|    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so\n*402|    // refresh covers it.\n 403|    // [impl->REQ-MESH-2]\n*404|    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());\n 405|    roster\n 406|        .save()\n 407|        .map_err(|e| JoinFail::Other(format!(\"roster save: {e}\")))?;\n...\n 475|        start,\n*476|        spt_net::net::pairing::ntp::force_refresh,\n 477|        Duration::from_millis(500),\n 478|    )\n 479|    .await\n...\n 495|/// query) and try ONE final probe before surrendering with the exhaustion\n*496|/// error. Generic over the probe + hook so the retry-once-after-refresh\n 497|/// behavior is unit-tested without a live `NetEndpoint`.\n 498|// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 499|async fn sweep_then_final_retry<A, E, P, Fut>(\n...\n 647|// [impl->REQ-MESH-2]\n*648|fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {\n 649|    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();\n 650|    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();\n 651|    let lease = spt_store::epoch::EpochSource::load().current();\n...\n 827|    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3 meet self-heal: on search-\n*828|    // deadline exhaustion the sweep forces ONE ceremony-clock refresh and tries\n*829|    // a final probe — proven here by a probe that FAILS until the refresh hook\n*830|    // fires, then succeeds. The final post-refresh sweep is what lands (the\n 831|    // stepped-clock join heals without a daemon bounce).\n 832|    #[tokio::test]\n*833|    async fn meet_retries_once_after_refresh_on_exhaustion() {\n 834|        use std::cell::Cell;\n*835|        let refreshed = Cell::new(false);\n\n[Showing lines 1-928 of 1143 (50.0KB limit). Use :929 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"crates/spt-daemon","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":236,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","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/config.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/notif.rs","crates/spt-daemon/src/pairhost.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/pump/mod.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":1},{"path":"crates/spt-daemon/src/applyhost.rs","count":20},{"path":"crates/spt-daemon/src/brain.rs","count":20},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/config.rs","count":3},{"path":"crates/spt-daemon/src/daemon.rs","count":15},{"path":"crates/spt-daemon/src/deadline.rs","count":20},{"path":"crates/spt-daemon/src/digest.rs","count":2},{"path":"crates/spt-daemon/src/digesthub.rs","count":4},{"path":"crates/spt-daemon/src/endpoint.rs","count":3},{"path":"crates/spt-daemon/src/lib.rs","count":7},{"path":"crates/spt-daemon/src/lifecycle.rs","count":20},{"path":"crates/spt-daemon/src/livehost.rs","count":20},{"path":"crates/spt-daemon/src/msg.rs","count":4},{"path":"crates/spt-daemon/src/nethost.rs","count":2},{"path":"crates/spt-daemon/src/notif.rs","count":3},{"path":"crates/spt-daemon/src/pairhost.rs","count":18},{"path":"crates/spt-daemon/src/projwriter.rs","count":14},{"path":"crates/spt-daemon/src/pump/mod.rs","count":20}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n## Cargo.toml\n 5│publish = false\n*6│description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) — the `spt` binary is a thin CLI over it. B0 lands the crate + the version...\n 7│\n 8│[dependencies]\n 9│# Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker\n\n## src/\n### applyhost.rs\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  33│//!    broker holds them all across the cycle). This replaces the old in-process\n *34│//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n  35│//!    subscriber while the live brain kept running the OLD code.\n  36│//!\n *37│//! New code runs **immediately** — the supervised respawn lands on the new\n  38│//! executable, no manual bounce, no logon. Readiness-gated promotion +\n  39│//! auto-rollback on a brain that fails to come up is D6.\n  40│// [impl->REQ-UPD-3]\n    │...\n  77│    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n *78│    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n  79│    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n  80│    /// provisional immediately (it cannot observe the asynchronous verdict — the\n  81│    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n    │...\n 122│/// the brain-only class gate. Pure w.r.t. the daemon — reads only the cache +\n*123│/// keys, swaps nothing. Both the broker-handoff apply and the daemonless apply\n 124│/// run this identical gate so their fail-closed posture never diverges.\n 125│// [impl->REQ-UPD-4]\n 126│fn prepare_apply(cache: &ReleaseCache, release_keys: &Path) -> Result<Prepared, String> {\n    │...\n 248│/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n*249│/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n 250│/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n*251│/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n 252│/// whether a running brain is cycled afterwards.\n 253│// [impl->REQ-UPD-6]\n 254│fn swap_and_record(\n    │...\n 273│    // carrying the last-known-good `rollback_binary` (the aside we just created)\n*274│    // so the broker's supervisor, a different process from this CLI, can promote\n 275│    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n 276│    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n 277│    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n*278│    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n 279│    // [impl->REQ-UPD-6]\n 280│    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n 281│        version,\n    │...\n 284│        // The version actually running before this swap — what a rollback returns\n*285│        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n 286│        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n 287│        // the quarantined version, not the running one. `None` when nothing was\n*288│        // ever recorded applied (a first-ever apply) → the supervisor falls back\n 289│        // to N-1, the only honest guess then.\n 290│        prior_version: cache.applied_version(),\n 291│    });\n    │...\n 297│/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n*298│/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299│/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300│/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n 301│// [impl->REQ-UPD-3]\n    │...\n 327│    // REQ-UPD-3's moment (ADR-0018 D3-3): trigger a real brain-PROCESS restart.\n*328│    // The broker signals its supervisor to cycle the brain child onto the\n 329│    // just-swapped binary with `start-reason=update`; new code runs immediately,\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 335│    //\n*336│    // A broker without a supervisor wired (`honored: false`) still swapped the\n 337│    // binary — the next daemon start uses the new executable. Readiness-gated\n 338│    // promotion + auto-rollback on a brain that fails to come up is D6.\n 339│    match brain.request_brain_restart() {\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    │...\n 360│/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n*361│/// mirrors the apply path: `false` means the broker has no supervisor wired\n 362│/// (accepted, not acted on).\n 363│// [impl->REQ-DAEMON-REFRESH]\n*364│pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365│    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366│    brain\n 367│        .request_brain_restart()\n    │...\n 379│/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n*380│/// the handoff path's does. Runs the identical verify/classify gate as\n 381│/// [`apply_staged`], so every fail-closed refusal is preserved.\n 382│// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n 383│// [impl->REQ-UPD-4]\n### brain.rs\n   8│//!\n  *9│//! - **`gen_start = now()` on cold-start *and* handoff (KNOWN-HAZARDS 2.4).**\n  10│//!   The wall-clock generation stamp is taken fresh on every start. A handed-off\n  11│//!   brain rehydrates its resume cursor and generation *counter* from prior\n  12│//!   state, but its `gen_start` is `now()`, **never** the stale value from that\n  13│//!   state — a rehydrated `gen_start` would fire time-based discriminators on\n *14│//!   the new generation. [`Brain::handoff`] takes the prior state by value yet\n  15│//!   ignores its `gen_start_ms`.\n  16│//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next\n *17│//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;\n  18│//!   the broker replays buffered output from there. [`Brain::read_event`] accepts\n  19│//!   only the contiguous next `seq` (advancing the cursor), silently drops an\n  20│//!   already-seen `seq` (a replay duplicate), and treats a forward gap as a hard\n    │...\n  68│\n *69│/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n  70│/// advance the generation. `gen_start_ms` is recorded for observability but is\n *71│/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n  72│/// fresh `gen_start` from `now()` (2.4).\n  73│///\n  74│/// **Test-only continuity (restoration D4-3, ADR-0018 Q6).** This brain→brain\n *75│/// frame is **retired from the production path**: a brain the broker's supervisor\n  76│/// respawns reconstructs continuity by querying the broker\n  77│/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n *78│/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n  79│/// solely because the integration tests (separate test target — `#[cfg(test)]`\n *80│/// would hide them) still drive the handoff-frame shape directly; there is **no\n  81│/// non-test caller** (KNOWN-HAZARDS 2.4 grep-clean close-out).\n  82│#[derive(Debug, Clone)]\n  83│pub struct BrainState {\n    │...\n  85│    pub session_id: u64,\n *86│    /// The generation counter (incremented on each handoff).\n  87│    pub generation: u64,\n  88│    /// The next output sequence still needed (the resume cursor).\n  89│    pub next_seq: u64,\n  90│    /// The prior generation's wall-clock start (ignored as authoritative on\n *91│    /// handoff — kept only to demonstrate it is *not* carried forward).\n  92│    pub gen_start_ms: u64,\n  93│}\n  94│\n    │...\n 204│/// channel supplies the deadline. On a timeout the pump bubbles out to a\n*205│/// supervised restart (a fresh [`Brain`] → fresh split → fresh reader thread);\n 206│/// the old reader, parked in `read_frame`, is abandoned (KH 7.6 leak watch — the\n 207│/// old conn's drop eventually errors its read and the thread exits).\n 208│enum BrainConn {\n    │...\n 216│        /// The reader-thread handle — held so the thread is owned, not detached\n*217│        /// silently. On a supervised restart the whole [`Brain`] drops; this\n 218│        /// handle drops with it (the thread, blocked in `read_frame`, is\n 219│        /// abandoned and exits when the old conn closes — KH 7.6).\n 220│        _reader: JoinHandle<()>,\n    │...\n 272│    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.\n*273│    /// Durability across a handoff is the logic layer's job (a re-attaching\n 274│    /// brain queries `net-streams` and re-subscribes from its durable cursors).\n 275│    net_cursors: std::collections::HashMap<u64, u64>,\n 276│    /// The presence resume cursor (D4c) — the conn-liveness sibling of\n    │...\n 283│    /// [`Brain::resume_sessions`], replacing the retired single-session\n*284│    /// `BrainState` handoff. A session present here is in **resume mode** — its\n 285│    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n 286│    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n 287│    /// boundary re-send) and accept-and-snap `seq >= cursor` (the contiguous case\n    │...\n 367│    ///\n*368│    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n*369│    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n 370│    /// integration tests) replays output through it with no later `attach`. So it\n 371│    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n 372│    /// ALSO subscribes from 0 over this same socket → two racing\n    │...\n 381│    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n*382│    /// never this brain→brain frame; `handoff` survives only for the integration\n 383│    /// tests. See [`BrainState`].\n 384│    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n*385│    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n 386│        let mut conn = LocalSocketTransport::connect(name)?;\n 387│        send_hello(&mut conn, Role::Brain)?;\n 388│        // DEFENSE-IN-DEPTH (P1c): seed the dedup cursor at the resume point so\n    │...\n 415│\n*416│    /// The generation counter (0 on cold start, +1 per handoff).\n 417│    pub fn generation(&self) -> u64 {\n 418│        self.generation\n 419│    }\n    │...\n 430│\n*431│    /// Snapshot durable state for a future handoff. **Test-only (restoration\n 432│    /// D4-3)** — the production path reconstructs from the broker, never a\n 433│    /// snapshot frame; see [`BrainState`].\n 434│    pub fn snapshot(&self) -> BrainState {\n### brainproc.rs\n  *1│//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n   2│//! broker/brain process-isolation restoration, D1 skeleton).\n   3│//!\n   4│//! ADR-0004 specified the broker/brain split as two **processes**; production\n    │...\n   7│//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n  *8│//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n   9│//! `spt daemon brain` child. The child connects over the existing broker socket\n  10│//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n  11│//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n    │...\n  18│//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n *19│//! broker spawns + supervises a brain child over the socket, and the broker\n  20│//! (with its PTY children + seed channel) survives the brain dying — the\n *21│//! supervisor respawns it from the executable path, which on a real update is\n  22│//! the swapped binary.\n  23│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n  24│\n    │...\n  37│/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n *38│/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n  39│/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\n  40│const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n  41│/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n    │...\n  51│/// (a transient first-boot hiccup should not abandon a good binary) before the\n *52│/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n  53│/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n  54│/// window-expiry triggers rollback.\n  55│const TRIAL_PREREADY_BUDGET: u32 = 3;\n    │...\n  61│const BRAIN_CONNECT_TRIES: u32 = 300;\n *62│/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n  63│const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n  64│\n  65│/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n    │...\n  74│#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n *75│pub enum StartReason {\n  76│    /// The broker's first brain spawn this broker lifetime.\n  77│    Cold,\n *78│    /// A supervised respawn after an unexpected brain exit.\n  79│    Crash,\n  80│    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n  81│    Update,\n    │...\n  83│\n *84│impl StartReason {\n  85│    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n  86│    pub fn as_arg(self) -> &'static str {\n  87│        match self {\n *88│            StartReason::Cold => \"cold\",\n *89│            StartReason::Crash => \"crash\",\n *90│            StartReason::Update => \"update\",\n  91│        }\n  92│    }\n  93│\n    │...\n  98│        match s {\n *99│            \"update\" => StartReason::Update,\n*100│            \"crash\" => StartReason::Crash,\n*101│            _ => StartReason::Cold,\n 102│        }\n 103│    }\n 104│}\n 105│\n*106│/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n*107│/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n 108│/// its next wait tick, kills the current brain child, and respawns it with\n 109│/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n*110│/// between the broker serve path and the supervisor — both run in the broker\n 111│/// process. This is the seam that makes a routine `apply` actually run new code,\n 112│/// the pillar the in-process-collapse regression silently unrealized.\n 113│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    │...\n 130│\n*131│    /// Consume a pending request (supervisor side): true at most once per\n 132│    /// `request`, clearing it so the next cycle starts fresh.\n 133│    pub fn take(&self) -> bool {\n 134│        self.requested.swap(false, Ordering::SeqCst)\n### broker.rs\n    9│//!\n  *10│//! ## Gapless handoff (B2 — the load-bearing invariant)\n   11│//!\n   12│//! A brain may be killed and restarted at any moment (a routine self-update, a\n   13│//! crash). The broker — and every hosted child — must survive that untouched,\n     │...\n  145│\n *146│/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n  147│/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n  148│/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n  149│/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n     │...\n  335│/// shared with the inject worker and RESET to 0 on a healthy `{commit}`, so a\n *336│/// respawned binary that recovers refreshes the whole budget (the C3(b) give-up-\n  337│/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n  338│/// psyche crash-loop backoff's bounded give-up.\n  339│const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n     │...\n  502│    by: Option<String>,\n *503│    /// Time of the last SUCCESSFUL handoff (`try_send` Ok) to this controller's\n  504│    /// writer channel (b4, REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE).\n  505│    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n  506│    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n     │...\n  516│    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n *517│    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n  518│    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n  519│    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n  520│    /// `last_ok` stale. `Arc<Mutex<…>>` so the reader observes the writer's live\n     │...\n  726│    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n *727│    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n  728│    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n  729│    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n  730│    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n *731│    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n  732│    /// (bounded wedge preserved). The writer still does the blocking socket write +\n  733│    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n  734│    /// single drain thread.\n     │...\n  824│        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n *825│        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n  826│        // slow controller falls behind the ring) unless it has been Full for the\n  827│        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n  828│        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n     │...\n 1507│// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n*1508│#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n 1509│fn controller_writer(\n 1510│    sid: u64,\n 1511│    send: SharedSend,\n     │...\n 1603│            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n*1604│            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n 1605│            // a seq (dropped while Full). The frame still went to the socket (the live\n 1606│            // operator sees it), but the durable resume cursor must NOT jump past the\n 1607│            // gap — see [`contiguous_advance`].\n     │...\n 1678│    /// increments it per rebuild, the worker RESETS it to 0 on a healthy `{commit}`\n*1679│    /// (so a recovered binary refreshes the whole budget). At `translate_respawn_\n 1680│    /// budget()` the daemon gives up (loud) and leaves the binary dead → spool.\n 1681│    translation_respawns: Arc<AtomicU32>,\n 1682│    /// The adapter name owning this session (W3a, ADR-0025) — empty for a\n     │...\n 1948│            thread::spawn(move || {\n*1949│                run_inject_worker_supervised(\n 1950│                    sup_child,\n 1951│                    sup_faulted,\n 1952│                    sup_endpoint,\n     │...\n 2008│// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\n*2009│fn run_inject_worker_supervised(\n 2010│    child: Arc<TranslationChild>,\n 2011│    faulted: Arc<AtomicBool>,\n 2012│    endpoint: String,\n     │...\n 2375│        if committed {\n*2376│            // Healthy sequence: reset the strike counter, refresh the respawn budget\n 2377│            // (a working binary heals it), and forget any earlier respool of THIS\n 2378│            // envelope (it succeeded → a future miss may respool it afresh).\n 2379│            strikes = 0;\n     │...\n 2540│    net: std::sync::OnceLock<NetHost>,\n*2541│    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542│    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543│    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n*2544│    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n*2545│    /// `Daemon::run` after it spawns the supervisor.\n 2546│    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,\n 2547│    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a\n 2548│    /// `session_id → (subnet, real address, 5-min deadline)` between\n     │...\n 2703│\n*2704│    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n 2705│    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n*2706│    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707│    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n### config.rs\n  27│\n *28│/// Default orphan-watch grace window (B7): how long the supervised-crash teardown\n  29│/// waits after observing a hosted Self's harness owner gone before it rechecks\n  30│/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n *31│/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n  32│/// teardown for long. Configurable per machine via `crash_grace_ms`.\n  33│pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n  34│\n    │...\n 137│    /// The orphan-watch grace window (B7): the recovery wait before a\n*138│    /// supervised-crash teardown rechecks liveness and signs off (1.1).\n 139│    pub crash_grace: Duration,\n 140│    /// Whether self-update is full-auto (C2, REQ-UPD-4). **Default `false`** — an\n 141│    /// update is gated on user confirmation delivered to the most-recently-active\n### daemon.rs\n 185│        let net_up = net.is_some();\n*186│        // Keep the broker handle alive past the bind so the brain supervisor's\n 187│        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188│        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189│        let broker_handle: Option<Arc<Broker>> =\n 190│            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191│                Ok(broker) => {\n    │...\n 311│\n*312│        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n 313│        // restoration): the broker is the always-up per-machine anchor; it spawns\n*314│        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\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    │...\n 322│        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n*323│        // container the supervisor enrolls every (re)spawned brain into. Created\n*324│        // here so it outlives the supervisor and is reachable from the\n 325│        // graceful-stop path below. The brain subtree (the brain + its shellwake\n 326│        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n 327│        // never in the container.\n    │...\n 329│        let reaper = crate::reap::BrainReaper::new();\n*330│        // The supervisor stop flag — hoisted out of the wiring block (was held\n 331│        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n*332│        // it, so the supervisor does not respawn the brain we are about to reap.\n 333│        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n 334│        {\n*335│            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n 336│                Arc::clone(&brain_stop),\n 337│                Arc::clone(&reaper),\n 338│                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n    │...\n 358│        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n*359│        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n 360│        // does not respawn the brain we are about to kill) THEN reap the brain\n 361│        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n 362│        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n    │...\n 371│    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n*372│    /// broker process spawns + supervises this; it connects to the broker over\n 373│    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n 374│    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n 375│    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n*376│    /// is parsed leniently ([`crate::brainproc::StartReason::from_arg`]) so an\n 377│    /// unknown value from a newer broker defaults to `Cold` (KH-2.3).\n 378│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 379│    pub fn run_brain(generation: u64, start_reason: &str) -> io::Result<()> {\n    │...\n 390│            generation,\n*391│            crate::brainproc::StartReason::from_arg(start_reason),\n 392│        )\n 393│    }\n 394│}\n    │...\n 430│\n*431│/// Background net self-heal supervisor (REQ-DAEMON-9): a daemon that came up\n 432│/// net-less (NET_BIND_FAIL at boot — typically the autostart racing the network\n 433│/// up) keeps retrying `try_start_net` with capped backoff and, on success,\n 434│/// ATTACHES the host to the live broker (`OnceLock`, so exactly once). The node\n### deadline.rs\n   3│//! Phase-significant periodic timing (the pulse loop) and one-shot deadlines\n  *4│//! (alarms) must survive a brain swap: the supervisor respawns a fresh brain\n   5│//! (`Brain::cold_start`) and the outgoing brain is gone before the new one starts\n   6│//! (D3-3), so continuity cannot ride a brain→brain frame — it must come from the\n   7│//! persistent side (disk), exactly as D4 moved session continuity to the broker.\n    │...\n  13│//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n *14│//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n *15│//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n  16│//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n *17│//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n  18│//!   IPC round-trip.\n  19│//!\n  20│//! - [`OneShotDeadline`] — an **absolute** `target_ms` fired once. Persisted at\n  21│//!   creation; **every** start reads it and fires-if-due; it is **never reset on\n *22│//!   any** `StartReason` (\"remind me at 3pm\" is a commitment that outlives any\n  23│//!   restart — the asymmetry vs the periodic crash-reset IS the rule). Built as a\n  24│//!   tested-but-unwired **pure helper** (ADR-0018 V3): the daemon has no one-shot\n  25│//!   consumer today (alarm is a legacy-listener in-memory timer,\n    │...\n  38│\n *39│use crate::brainproc::StartReason;\n  40│use crate::config::DEFAULT_PULSE_PERIOD;\n  41│\n  42│/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n    │...\n  77│\n *78│    /// Load-or-initialize the anchor for `key`, applying the [`StartReason`]\n  79│    /// rehydration rule (ADR-0018 Q4):\n  80│    ///\n *81│    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n *82│    ///   only the interval refreshes from live config. If no file exists (updating\n  83│    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n  84│    ///   — never fail an update on a missing file.\n *85│    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n  86│    ///   at `now_ms` (phase reset acceptable; cold is the first-ever start).\n  87│    ///\n  88│    /// A corrupt file is treated as absent (degrade-to-fresh), matching\n    │...\n  94│        interval_ms: u64,\n *95│        reason: StartReason,\n  96│        now_ms: u64,\n  97│    ) -> std::io::Result<Self> {\n  98│        let path = anchor_path(key);\n    │...\n 100│        let anchor = match (reason, Self::load(&path)) {\n*101│            // Update keeps the persisted phase; the interval refreshes to config.\n*102│            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103│                anchor_ms: prev.anchor_ms,\n 104│                interval_ms,\n 105│            },\n    │...\n 180│/// **rule** as a tested-but-unwired pure helper: persist `target_ms` at creation,\n*181│/// fire-if-due on every start, **never reset** on any [`StartReason`]. No\n 182│/// scheduler/timer/thread lives here; the durable in-daemon alarm scheduler that\n 183│/// will drive it is the alarm-port deferral (`docs/DEFERRED.md`). [`fire_if_due`]\n*184│/// takes **no** `StartReason` — that absence IS the never-reset rule, made\n 185│/// explicit in code so the alarm port cannot re-derive the crash-asymmetry wrong.\n 186│///\n 187│/// [`fire_if_due`]: OneShotDeadline::fire_if_due\n    │...\n 303│        with_home(|_| {\n*304│            let cold = DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 305│            assert_eq!(cold.anchor_ms, 1_000);\n 306│            // A crash start rewrites the anchor to the restart instant (phase reset).\n*307│            let crash = DeadlineAnchor::open(\"pulse\", 100, StartReason::Crash, 9_999).unwrap();\n 308│            assert_eq!(crash.anchor_ms, 9_999);\n 309│        });\n 310│    }\n    │...\n 312│    #[test] // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*313│    fn update_keeps_anchor_and_refreshes_interval() {\n 314│        with_home(|_| {\n*315│            DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 316│            // Update preserves phase (anchor) but the interval tracks live config.\n*317│            let upd = DeadlineAnchor::open(\"pulse\", 250, StartReason::Update, 9_999).unwrap();\n 318│            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n### digest.rs\n 117│/// `from` points at the changed tail (a minimal delta); a window slide evicts the\n*118│/// oldest turn → the heads differ from index 0 → `from == 0` (a full refresh).\n 119│pub fn common_prefix_len(a: &[Turn], b: &[Turn]) -> usize {\n 120│    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()\n 121│}\n    │...\n 531│    // [unit->REQ-TERM-4] the diff base: a shared head yields its length (a tail\n*532│    // delta); a divergent front yields 0 (a full refresh).\n 533│    #[test]\n 534│    fn common_prefix_len_finds_the_unchanged_head() {\n 535│        let a = vec![turn(\"a\"), turn(\"b\")];\n### digesthub.rs\n 121│        // The changed turns begin past the equal-by-value head; a window slide\n*122│        // (front eviction) diverges at 0 → a full refresh.\n 123│        let from = common_prefix_len(&st.snapshot.turns, &new.turns);\n 124│        st.version += 1;\n 125│        let update = DigestUpdate {\n    │...\n 463│    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a\n*464│    // tail delta (from past the unchanged head); a window slide refreshes from 0.\n 465│    #[test]\n*466│    fn publish_delta_is_tail_then_full_refresh() {\n 467│        let hub = DigestHub::new();\n 468│        let (_b, _v, rx) = hub.subscribe(\"doyle\");\n 469│        hub.project_and_publish(\"doyle\", digest(vec![turn(\"a\"), turn(\"b\")]));\n    │...\n 477│        assert_eq!(up.turns[0].input.as_deref(), Some(\"c\"));\n*478│        // A front eviction (window slide) diverges at 0 → full refresh.\n 479│        hub.project_and_publish(\"doyle\", digest(vec![turn(\"b\"), turn(\"c\"), turn(\"d\")]));\n 480│        let up = rx.recv().unwrap();\n 481│        assert_eq!(up.from, 0, \"front eviction resends from 0\");\n### endpoint.rs\n 35│\n*36│/// The broker listener name (B1/B2): PTY hosting + gapless handoff.\n 37│pub fn broker_socket_name() -> String {\n 38│    format!(\"spt-daemon-broker-{}.sock\", home_tag())\n 39│}\n   │...\n 75│/// child's pid, rewritten each heartbeat). The broker spawns the brain as a\n*76│/// supervised child process (ADR-0018 Q2/Q3 — the two-process restoration); the\n*77│/// child writes this once it has connected to the broker socket, and refreshes\n 78│/// it while attached. A test (or a future readiness-gated rollback, ADR-0018\n 79│/// Q7) reads it to confirm the brain came up and — on a pid change — that it\n 80│/// respawned. Best-effort like the daemon pid breadcrumb; liveness of the\n### lib.rs\n  *1│//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).\n   2│//!\n   3│//! ADR-0004 splits the logical daemon into two implementation layers so a\n   4│//! routine self-update swaps the logic without disturbing live endpoints:\n    │...\n  45│//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).\n *46│//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →\n  47│//! drop → re-attach — so the live PTY child + its output stream survive the logic\n *48│//! swap untouched; it never re-implements the handoff and never touches a broker\n  49│//! resource.\n  50│//!\n *51│//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a\n  52│//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback\n  53│//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key\n  54│//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a\n    │...\n  97│//!\n *98│//! The brain + gapless handoff (B2) and the rest of the M3b task table build on\n *99│//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK\n 100│//! (R-ARCH-2).\n 101│//!\n 102│//! [`Envelope`]: frame::Envelope\n    │...\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### lifecycle.rs\n   35│\n  *36│use crate::brainproc::StartReason;\n   37│use crate::config::DaemonConfig;\n   38│use crate::deadline::DeadlineAnchor;\n   39│use crate::notif::NotifSurfacePolicy;\n     │...\n  307│/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n *308│/// **refresh** them while the brain driver thread keeps running. The\n  309│/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n  310│/// otherwise unreachable from an IPC apply handler — so ADR-0025's \"re-clone the\n  311│/// manifest into the running BrainLifecycle\" is realized as a clonable handle the\n     │...\n  331│    /// never holds the read lock across the spawn/blocking call — a concurrent\n *332│    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n  333│    fn runtime_snapshot(&self) -> ManifestRuntime {\n  334│        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n  335│    }\n     │...\n  342│    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n *343│    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n  344│    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n  345│    /// brain thread, PTY, broker session, and output log are untouched; only these\n  346│    /// two cells' contents change. (W3c)\n  347│    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n *348│    pub fn refresh(&self, new: Manifest, install_dir: Option<&Path>) {\n  349│        let runtime = build_runtime(&new, install_dir);\n  350│        // Write the runtime first, then the manifest: a reader that snapshots\n  351│        // between the two writes sees the new runtime with the old manifest at\n     │...\n  371│pub struct BrainLifecycle {\n *372│    /// Manifest + runtime behind a refreshable shared handle (W3c).\n  373│    cell: ManifestCell,\n  374│    /// The adapter install dir, retained for the W3c reload-on-dirty poll\n *375│    /// (`<install_dir>/manifest.toml`) and to re-pin the runtime on refresh.\n  376│    /// `None` ⇒ PATH-only resolution + no live-reload (a test/legacy host).\n  377│    install_dir: Option<PathBuf>,\n  378│    id: String,\n     │...\n  437│        // psyche role's bare program token resolves there before PATH (a\n *438│        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n  439│        Some(BrainLifecycle {\n  440│            cell: ManifestCell::new(manifest.clone(), install_dir),\n  441│            install_dir: install_dir.map(Path::to_path_buf),\n     │...\n  450│\n *451│    /// Refresh this endpoint's manifest + runtime in place — a thin delegate to\n  452│    /// the shared cell. The production trigger is [`reload_manifest_if_changed`]\n  453│    /// (the brain pulling an on-disk swap); this direct entry exists for tests and\n  454│    /// any in-process caller. Brain-parity: nothing else cycles. (W3c)\n  455│    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n *456│    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {\n *457│        self.cell.refresh(new, install_dir);\n  458│    }\n  459│\n  460│    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\n     │...\n  470│    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\n *471│    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\n  472│    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\n  473│    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\n  474│    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\n     │...\n  498│            Some(new) => {\n *499│                self.cell.refresh(new, Some(dir));\n  500│                *last = Some(hash);\n  501│            }\n  502│            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\n     │...\n  581│\n *582│    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n  583│    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n  584│    /// echo-commune (T3).\n  585│    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n     │...\n  587│        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n *588│        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n  589│        // `last_active_ms` — a resting seat ticking its pulse must never win\n  590│        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n  591│        // the notif first-fire). A dormant seat instead checks the opt-in\n     │...\n  682│    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\n *683│    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\n  684│    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\n  685│    /// deadline is derived functionally — **no per-fire write**.\n  686│    ///\n     │...\n  695│        stop: &AtomicBool,\n *696│        reason: StartReason,\n  697│        mut on_tick: F,\n  698│    ) {\n  699│        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\n     │...\n  714│        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n *715│        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n  716│        // the initial state.\n  717│        let mut manifest_hash = self.manifest_disk_hash();\n  718│        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n     │...\n  755│        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n *756│        // history fetch below), so a live-update refresh never waits on it.\n  757│        let manifest = self.cell.manifest_snapshot();\n  758│        let Some(h) = &manifest.history else {\n  759│            return Vec::new();\n     │...\n 1213│    /// the Self is leaving on purpose, there is nothing to recover. The\n*1214│    /// supervised-crash path ([`supervised_crash_signoff`](Self::supervised_crash_signoff))\n 1215│    /// supplies the real grace + liveness recheck for an *un*intentional death.\n 1216│    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {\n 1217│        self.signoff_with(session_id, Duration::ZERO, || true)\n### livehost.rs\n    5│//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n   *6│//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n    7│//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n    8│//! disk): same shape — enumerate Self perches, resolve each manifest from its\n   *9│//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run\n   10│//! one bounded reconcile sweep at boot and every tick. Where the shell waker\n   11│//! hosts **offline** shells, this hosts **online** live endpoints — `status` is\n   12│//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the\n     │...\n   38│use crate::brain::{now_ms, Brain};\n  *39│use crate::brainproc::StartReason;\n   40│use crate::config::DaemonConfig;\n   41│use crate::lifecycle::BrainLifecycle;\n   42│\n     │...\n  150│    cfg: &DaemonConfig,\n *151│    reason: StartReason,\n  152│) {\n  153│    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n  154│    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n     │...\n  613│/// daemon online-stamp makes liveness daemon-authoritative), then start the\n *614│/// config-paced pulse driver on its own supervised thread with a held stop flag.\n  615│/// A Psyche-spawn failure tables nothing — the next sweep retries.\n  616│// [impl->REQ-DAEMON-1]\n  617│fn host_one(\n     │...\n  621│    session_id: String,\n *622│    reason: StartReason,\n  623│) {\n  624│    lifecycle.sweep_on_start();\n  625│    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n     │...\n  977│// [impl->REQ-DAEMON-1]\n *978│pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n  979│    std::thread::spawn(move || {\n  980│        let set = Arc::new(LiveSet::new());\n  981│        {\n     │...\n 1316│                &cfg,\n*1317│                StartReason::Cold,\n 1318│            );\n 1319│\n 1320│            let status = |id: &str| {\n     │...\n 1391│                &fast_cfg(),\n*1392│                StartReason::Cold,\n 1393│            );\n 1394│\n 1395│            let exists = |wid: &str| {\n     │...\n 1573│                &cfg,\n*1574│                StartReason::Cold,\n 1575│            );\n 1576│            assert_eq!(set.len(), 1, \"the online live endpoint is hosted\");\n 1577│\n     │...\n 1608│                &fast_cfg(),\n*1609│                StartReason::Cold,\n 1610│            );\n 1611│            assert!(set.is_empty(), \"an offline live endpoint is never hosted\");\n 1612│        });\n     │...\n 1633│                    &cfg,\n*1634│                    StartReason::Cold,\n 1635│                )\n 1636│            };\n 1637│            run();\n     │...\n 1664│                &cfg,\n*1665│                StartReason::Cold,\n 1666│            );\n 1667│            assert_eq!(first.len(), 1);\n 1668│            // Brain \"restart\": drop the table (threads die with the process), the\n     │...\n 1680│                &cfg,\n*1681│                StartReason::Crash,\n 1682│            );\n 1683│            assert_eq!(second.len(), 1, \"a fresh brain re-hosts the online endpoint\");\n 1684│            set_then_teardown(&second);\n     │...\n 1719│            };\n*1720│            run(StartReason::Cold);\n 1721│            assert_eq!(set.len(), 1, \"hosted while online\");\n 1722│\n 1723│            // Graceful signoff flips the Self perch offline WHILE the driver pulses.\n     │...\n 1726│\n*1727│            run(StartReason::Cold);\n 1728│            assert!(\n 1729│                set.is_empty(),\n 1730│                \"an offline-transitioned endpoint is un-hosted (driver stopped + de-tabled)\"\n     │...\n 1755│                    &cfg,\n*1756│                    StartReason::Cold,\n 1757│                )\n 1758│            };\n 1759│            run();\n     │...\n 1991│                &fast_cfg(),\n*1992│                StartReason::Cold,\n 1993│            );\n 1994│            let perch = perch::resolve_perch_path(\"cold\", ParentHint::Infer);\n 1995│            let info = spt_store::info::read_info(&perch).unwrap();\n     │...\n 2028│                &fast_cfg(),\n*2029│                StartReason::Cold,\n 2030│            );\n 2031│            let info = spt_store::info::read_info(&perch).unwrap();\n 2032│            assert!(info.host_error.is_none(), \"no material → benign, not an error\");\n     │...\n 2052│                &fast_cfg(),\n*2053│                StartReason::Cold,\n 2054│            );\n 2055│            let info = spt_store::info::read_info(&perch).unwrap();\n 2056│            assert!(info.host_error.is_none(), \"not woken → no attempt, no error\");\n### msg.rs\n 226│/// Q3). `spt update apply` sends this after swapping the binary on disk; the\n*227│/// broker signals its brain supervisor to cycle the brain child with\n 228│/// `start-reason=update`, so the live brain restarts onto the new binary while\n 229│/// every broker-held endpoint stays untouched at the process level. Additive\n 230│/// verb (KH-2.3): an older broker that predates it answers with a generic error,\n    │...\n 233│/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n*234│/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235│/// acceptance, not completion. `honored` is false when the broker has no\n*236│/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237│/// reached a broker that cannot itself respawn.\n 238│pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239│\n    │...\n 872│/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n*873│/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874│/// cycle the child; false when the broker has none wired (a bare test broker),\n 875│/// so the caller can tell a real planned restart from a mere accept.\n 876│#[derive(Debug, Clone, Serialize, Deserialize)]\n### nethost.rs\n  68│/// continues, heartbeat advances), NOT the brain's own read-deadline (the A-half\n *69│/// poison → supervised-restart path). The 20s margin below 30s covers the error\n  70│/// frame's IPC transit + the brain's read scheduling. Test-overridable via\n  71│/// [`NetHost::set_quic_op_timeout`] so the hermetic black-hole tests fail fast.\n  72│const BROKER_QUIC_OP_TIMEOUT_MS: u64 = 10_000;\n    │...\n 988│    /// round continues, heartbeat advances) — DISTINCT from the brain's OWN\n*989│    /// read-deadline `TimedOut` (the A-half poison → supervised-restart signal).\n 990│    /// Because the bound fires (10s) well before the brain's 30s\n 991│    /// `PUMP_PEER_IO_TIMEOUT`, the broker always REPLIES first. A live op\n 992│    /// completes with zero added latency; the bound only bites a non-responsive\n### notif.rs\n  57│/// Producer kind for the auto-rollback alert (ADR-0018 Q7, restoration D6-2): a\n *58│/// brain-only update failed readiness and the supervisor rolled back to the\n  59│/// last-known-good binary. A distinct kind so a surface can style it; it rides\n  60│/// the same first-fire + **boundary-resurface** path as every other producer, so\n  61│/// an undismissed rollback row re-surfaces until the operator dismisses it\n    │...\n 426│/// Q7): a brain-only update `v{quarantine_version}` failed to reach `ready` and\n*427│/// the supervisor rolled back to `v{running_version}`, quarantining the bad\n 428│/// version. Reuses the one producer front door ([`produce_and_first_fire`]) so\n 429│/// the row rides first-fire AND **boundary-resurface** — an undismissed rollback\n 430│/// row re-surfaces at every boundary until the operator dismisses it (the\n 431│/// \"loud, not fire-once\" requirement; no new persistence path). The broker's\n*432│/// supervisor calls this once per member subnet on rollback.\n 433│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 434│#[allow(clippy::too_many_arguments)]\n 435│pub fn produce_rollback_notif(\n### pairhost.rs\n  76│    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);\n *77│    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every\n  78│    // served subnet so the joiner always learns at least us, then hand it the\n  79│    // whole current roster in the Seed frame.\n  80│    let mut roster = RosterStore::load();\n *81│    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());\n  82│    match tokio::time::timeout(\n  83│        CEREMONY_TIMEOUT,\n  84│        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),\n    │...\n 401│    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so\n*402│    // refresh covers it.\n 403│    // [impl->REQ-MESH-2]\n*404│    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());\n 405│    roster\n 406│        .save()\n 407│        .map_err(|e| JoinFail::Other(format!(\"roster save: {e}\")))?;\n    │...\n 475│        start,\n*476│        spt_net::net::pairing::ntp::force_refresh,\n 477│        Duration::from_millis(500),\n 478│    )\n 479│    .await\n    │...\n 495│/// query) and try ONE final probe before surrendering with the exhaustion\n*496│/// error. Generic over the probe + hook so the retry-once-after-refresh\n 497│/// behavior is unit-tested without a live `NetEndpoint`.\n 498│// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 499│async fn sweep_then_final_retry<A, E, P, Fut>(\n    │...\n 647│// [impl->REQ-MESH-2]\n*648│fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {\n 649│    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();\n 650│    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();\n 651│    let lease = spt_store::epoch::EpochSource::load().current();\n    │...\n 827│    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3 meet self-heal: on search-\n*828│    // deadline exhaustion the sweep forces ONE ceremony-clock refresh and tries\n*829│    // a final probe — proven here by a probe that FAILS until the refresh hook\n*830│    // fires, then succeeds. The final post-refresh sweep is what lands (the\n 831│    // stepped-clock join heals without a daemon bounce).\n 832│    #[tokio::test]\n*833│    async fn meet_retries_once_after_refresh_on_exhaustion() {\n 834│        use std::cell::Cell;\n*835│        let refreshed = Cell::new(false);\n 836│        let calls = Cell::new(0u32);\n 837│        let probe = || async {\n 838│            calls.set(calls.get() + 1);\n*839│            if refreshed.get() {\n 840│                Ok(7u32)\n 841│            } else {\n 842│                Err(\"no seed-holder\")\n    │...\n 850│            start,\n*851│            || refreshed.set(true),\n 852│            Duration::from_millis(1),\n 853│        )\n 854│        .await;\n*855│        assert_eq!(got, Ok(7u32), \"the post-refresh final sweep lands\");\n*856│        assert_eq!(calls.get(), 2, \"one exhausting probe + one post-refresh retry\");\n*857│        assert!(refreshed.get(), \"the refresh hook fired between the two probes\");\n 858│    }\n 859│\n*860│    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] and if the final post-refresh sweep\n 861│    // ALSO fails, surrender with the richer exhaustion error — exactly ONE\n 862│    // retry, never an unbounded loop.\n 863│    #[tokio::test]\n### projwriter.rs\n   18│//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n  *19│//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n   20│//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n   21│//!   scan;\n   22│//! - maintains an observability sidecar (`project-index-stats.json`) — index\n   23│//!   presence alone is not health: generated time, source generation, pending\n  *24│//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n   25│//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n   26│//!   (wall-clock budgets are manual acceptance ONLY).\n   27│// [impl->REQ-PROJECT-INDEX-WRITER]\n     │...\n   90│    pub source_generation: String,\n  *91│    /// Whether a refresh is queued/underway right now.\n   92│    #[serde(default)]\n  *93│    pub pending_refresh: bool,\n   94│    /// When the last reconcile cycle ran (epoch ms).\n   95│    #[serde(default)]\n   96│    pub last_run_ms: u64,\n     │...\n  179│/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n *180│/// for a linked worktree). Identity refreshes ONLY when the marker path or\n  181│/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\n  182│/// §3: commits do not alter project identity).\n  183│#[derive(Debug, Clone)]\n     │...\n  245│            source_generation: last_fingerprint.clone(),\n *246│            pending_refresh: true, // the boot reconcile is queued by definition\n  247│            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n  248│            ..IndexWriterStats::default()\n  249│        };\n     │...\n  518│        self.stats.last_run_ms = now_ms();\n *519│        self.stats.pending_refresh = true; // the work is still owed\n  520│        self.write_stats();\n  521│    }\n  522│\n     │...\n  528│        self.stats.last_error = None;\n *529│        self.stats.pending_refresh = false;\n  530│        self.stats.cwds = self.cwd_cache.len() as u64;\n  531│        self.write_stats();\n  532│    }\n     │...\n  666│                // Debounce: let the burst land, then drain it ALL — the\n *667│                // coalescing window (multiple invalidations → one refresh).\n  668│                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n  669│                std::thread::sleep(debounce);\n *670│                engine.stats.pending_refresh = true;\n  671│                engine.write_stats();\n  672│            }\n  673│            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n     │...\n  894│        assert!(stats.last_error.is_some(), \"the failure is an observable fact\");\n *895│        assert!(stats.pending_refresh, \"the owed work stays visible\");\n  896│    }\n  897│\n  898│    // [unit->REQ-PROJECT-INDEX-INVALIDATION] scoped invalidation: an endpoint\n     │...\n 1080│            // Wait for the boot cycle to settle.\n*1081│            while stats().map(|s| s.pending_refresh || s.generated_ms == 0).unwrap_or(true) {\n 1082│                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n 1083│                std::thread::sleep(Duration::from_millis(25));\n 1084│            }\n     │...\n 1095│            while spt_store::projinval::pending_at(&spt_store::projinval::invalidations_dir())\n*1096│                || stats().map(|s| s.pending_refresh).unwrap_or(true)\n 1097│            {\n 1098│                assert!(Instant::now() < consumed, \"burst never consumed\");\n 1099│                std::thread::sleep(Duration::from_millis(25));\n     │...\n 1266│        assert_eq!(stats.endpoints, 1);\n*1267│        assert!(!stats.pending_refresh);\n 1268│    }\n 1269│}\n\n### pump/\n#### mod.rs\n  104│/// way). A read that exceeds this is treated as a poisoned client and\n *105│/// escalated to a supervised restart, NEVER a per-peer retry.\n  106│const PUMP_PEER_IO_TIMEOUT: Duration = Duration::from_secs(30);\n  107│\n  108│/// Supervisor backoff floor: the first restart after a pump death.\n     │...\n  125│/// Per-peer dial-backoff cap (G5): a persistently-dead peer is re-dialed at most\n *126│/// every 5 minutes (same bound as the supervisor) — bounded, never a hot redial\n  127│/// loop, never silent. Reset to the floor on any successful connect.\n  128│const PEER_BACKOFF_CAP: Duration = Duration::from_secs(300);\n  129│\n     │...\n  140│/// `HashMap<peer_hex, PeerSched>` on the pump loop — RAM-only, re-primed on a\n *141│/// supervised restart like `WorkerLasts`.\n  142│#[derive(Clone, Copy, Debug)]\n  143│struct PeerSched {\n  144│    next_due: Instant,\n     │...\n  181│    /// the stream: reader-thread `UnexpectedEof`/`BrokenPipe` on a closed conn).\n *182│    /// The re-pointed REQ-HAZARD-PUMP-IPC-DEADLINE: bubble to a supervised restart.\n  183│    Restart(io::Error),\n  184│}\n  185│\n     │...\n  189│/// a reclassified per-peer drop), but a dead BROKER — the event carrier itself\n *190│/// going silent — MUST still bubble to a supervised restart. `TimedOut` is the\n  191│/// benign \"no outcome available this instant\" (round closes on its deadline); any\n  192│/// OTHER read error is the carrier dying and MUST NOT be swallowed as quiet.\n  193│// [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n     │...\n  531│    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n *532│    // hfenduleam stall). A timed-out read escalates to a supervised restart\n  533│    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n  534│    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n  535│    let mut brain = connect(broker_name)?;\n     │...\n  632│                        // A raw carrier-op TimedOut on the submit IPC itself is a\n *633│                        // genuine broker-IPC desync → bubble → supervised restart.\n  634│                        Err(e) if e.kind() == io::ErrorKind::TimedOut => return Err(e),\n  635│                        // The broker refused the submit (net disabled / bad addr):\n  636│                        // an ordinary per-peer failure — back off, round continues.\n     │...\n  651│        // poison. `TimedOut` = quiet (drain done); any other read error = the\n *652│        // broker stopped answering the stream (dead carrier) → bubble → supervised\n  653│        // restart (the re-pointed REQ-HAZARD-PUMP-IPC-DEADLINE: a dead BROKER still\n  654│        // restarts the pump; a dead PEER never does).\n  655│        // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n     │...\n  760│/// after each — an ordinary failure drops the conn + backs the peer off and stops\n *761│/// this peer's remaining legs; a carrier desync bubbles to a supervised restart.\n  762│/// Each worker mints its open-ops only through the shell-owned [`PeerIo`].\n  763│// [impl->REQ-PUMP-PEER-ISOLATION]\n  764│#[allow(clippy::too_many_arguments)]\n     │...\n 1024│\n*1025│/// The supervisor's next backoff after a death: a healthy run (survived\n 1026│/// [`SUPERVISE_HEALTHY_RUN`]) resets to the floor `base`; a fast death\n 1027│/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`] (M8 decision 23 — a\n 1028│/// persistently-crashing pump stays visible and bounded, never silent and\n     │...\n 1048│// [impl->REQ-DAEMON-5]\n*1049│pub fn supervise_pump(stop: &AtomicBool, base: Duration, mut body: impl FnMut() -> io::Result<()>) {\n 1050│    let mut backoff = base;\n 1051│    while !stop.load(Ordering::Relaxed) {\n 1052│        let started = Instant::now();\n     │...\n 1073│        eprintln!(\n*1074│            \"PEER_PUMP_RESTART: supervised restart in {}s\",\n 1075│            backoff.as_secs()\n 1076│        );\n 1077│        let mut slept = Duration::ZERO;\n     │...\n 1086│/// daemon-boot entry ([`crate::daemon::Daemon::run`]). The thread body is\n*1087│/// the SUPERVISOR ([`supervise_pump`]), not the bare loop: the pump\n 1088│/// restarts after a panic or error with capped backoff (M8 decision 23).\n 1089│// [impl->REQ-DAEMON-5]\n 1090│pub fn spawn_peer_pump(\n     │...\n 1097│        let paths = PumpPaths::production();\n*1098│        supervise_pump(&stop, SUPERVISE_BACKOFF_BASE, || {\n 1099│            run_peer_pump(\n 1100│                &broker_name,\n 1101│                Arc::clone(&registry),\n     │...\n 1406│    // peer off (Q3), round continues (Ok); a genuine carrier-op raw TimedOut STILL\n*1407│    // bubbles Err → supervised restart. A dead PEER never restarts the pump; a\n 1408│    // dead BROKER conn still does.\n 1409│    #[test]\n 1410│    fn peer_leg_outcome_isolates_ordinary_backs_off_carrier_timeout_still_poisons() {\n     │...\n 1440│\n*1441│        // A genuine carrier-op raw TimedOut STILL poisons → supervised restart.\n 1442│        let carrier = io::Error::new(io::ErrorKind::TimedOut, \"brain IPC carrier desync\");\n 1443│        let bubbled = peer_leg_outcome(Err(carrier), \"cc\", &mut conns, &mut sched, now);\n 1444│        assert!(bubbled.is_err(), \"a real carrier desync still restarts the pump\");\n     │...\n 1564│        // A dead broker mid-drain: the reader thread ended → UnexpectedEof (or a\n*1565│        // BrokenPipe on the closed socket) → MUST bubble to a supervised restart.\n 1566│        assert!(matches!(\n 1567│            classify_drain_read(Err(io::Error::new(\n 1568│                io::ErrorKind::UnexpectedEof,\n     │...\n 1600│    #[test]\n*1601│    fn supervisor_backoff_doubles_caps_and_resets() {\n 1602│        let base = SUPERVISE_BACKOFF_BASE;\n 1603│        let fast = Duration::from_millis(10); // died immediately\n 1604│        let mut b = base;\n     │...\n 1619│\n*1620│    // [unit->REQ-DAEMON-5] the supervisor catches a PANICKING body and\n 1621│    // restarts it (with backoff) instead of dying silent — the 5.9-class\n*1622│    // half-death self-heals; raising stop ends supervision promptly.\n 1623│    #[test]","truncation":{"content":"# crates/spt-daemon/\n## Cargo.toml\n 5|publish = false\n*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) — the `spt` binary is a thin CLI over it. B0 lands the crate + the version...\n 7|\n 8|[dependencies]\n 9|# Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker\n\n## src/\n### applyhost.rs\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 33|//!    broker holds them all across the cycle). This replaces the old in-process\n*34|//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n 35|//!    subscriber while the live brain kept running the OLD code.\n 36|//!\n*37|//! New code runs **immediately** — the supervised respawn lands on the new\n 38|//! executable, no manual bounce, no logon. Readiness-gated promotion +\n 39|//! auto-rollback on a brain that fails to come up is D6.\n 40|// [impl->REQ-UPD-3]\n...\n 77|    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n*78|    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n 79|    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n 80|    /// provisional immediately (it cannot observe the asynchronous verdict — the\n 81|    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n...\n 122|/// the brain-only class gate. Pure w.r.t. the daemon — reads only the cache +\n*123|/// keys, swaps nothing. Both the broker-handoff apply and the daemonless apply\n 124|/// run this identical gate so their fail-closed posture never diverges.\n 125|// [impl->REQ-UPD-4]\n 126|fn prepare_apply(cache: &ReleaseCache, release_keys: &Path) -> Result<Prepared, String> {\n...\n 248|/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n*249|/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n 250|/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n*251|/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n 252|/// whether a running brain is cycled afterwards.\n 253|// [impl->REQ-UPD-6]\n 254|fn swap_and_record(\n...\n 273|    // carrying the last-known-good `rollback_binary` (the aside we just created)\n*274|    // so the broker's supervisor, a different process from this CLI, can promote\n 275|    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n 276|    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n 277|    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n*278|    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n 279|    // [impl->REQ-UPD-6]\n 280|    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n 281|        version,\n...\n 284|        // The version actually running before this swap — what a rollback returns\n*285|        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n 286|        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n 287|        // the quarantined version, not the running one. `None` when nothing was\n*288|        // ever recorded applied (a first-ever apply) → the supervisor falls back\n 289|        // to N-1, the only honest guess then.\n 290|        prior_version: cache.applied_version(),\n 291|    });\n...\n 297|/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n*298|/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299|/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n 301|// [impl->REQ-UPD-3]\n...\n 327|    // REQ-UPD-3's moment (ADR-0018 D3-3): trigger a real brain-PROCESS restart.\n*328|    // The broker signals its supervisor to cycle the brain child onto the\n 329|    // just-swapped binary with `start-reason=update`; new code runs immediately,\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 335|    //\n*336|    // A broker without a supervisor wired (`honored: false`) still swapped the\n 337|    // binary — the next daemon start uses the new executable. Readiness-gated\n 338|    // promotion + auto-rollback on a brain that fails to come up is D6.\n 339|    match brain.request_brain_restart() {\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...\n 360|/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n*361|/// mirrors the apply path: `false` means the broker has no supervisor wired\n 362|/// (accepted, not acted on).\n 363|// [impl->REQ-DAEMON-REFRESH]\n*364|pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365|    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366|    brain\n 367|        .request_brain_restart()\n...\n 379|/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n*380|/// the handoff path's does. Runs the identical verify/classify gate as\n 381|/// [`apply_staged`], so every fail-closed refusal is preserved.\n 382|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n 383|// [impl->REQ-UPD-4]\n### brain.rs\n 8|//!\n*9|//! - **`gen_start = now()` on cold-start *and* handoff (KNOWN-HAZARDS 2.4).**\n 10|//!   The wall-clock generation stamp is taken fresh on every start. A handed-off\n 11|//!   brain rehydrates its resume cursor and generation *counter* from prior\n 12|//!   state, but its `gen_start` is `now()`, **never** the stale value from that\n 13|//!   state — a rehydrated `gen_start` would fire time-based discriminators on\n*14|//!   the new generation. [`Brain::handoff`] takes the prior state by value yet\n 15|//!   ignores its `gen_start_ms`.\n 16|//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next\n*17|//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;\n 18|//!   the broker replays buffered output from there. [`Brain::read_event`] accepts\n 19|//!   only the contiguous next `seq` (advancing the cursor), silently drops an\n 20|//!   already-seen `seq` (a replay duplicate), and treats a forward gap as a hard\n...\n 68|\n*69|/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n 70|/// advance the generation. `gen_start_ms` is recorded for observability but is\n*71|/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n 72|/// fresh `gen_start` from `now()` (2.4).\n 73|///\n 74|/// **Test-only continuity (restoration D4-3, ADR-0018 Q6).** This brain→brain\n*75|/// frame is **retired from the production path**: a brain the broker's supervisor\n 76|/// respawns reconstructs continuity by querying the broker\n 77|/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n*78|/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n 79|/// solely because the integration tests (separate test target — `#[cfg(test)]`\n*80|/// would hide them) still drive the handoff-frame shape directly; there is **no\n 81|/// non-test caller** (KNOWN-HAZARDS 2.4 grep-clean close-out).\n 82|#[derive(Debug, Clone)]\n 83|pub struct BrainState {\n...\n 85|    pub session_id: u64,\n*86|    /// The generation counter (incremented on each handoff).\n 87|    pub generation: u64,\n 88|    /// The next output sequence still needed (the resume cursor).\n 89|    pub next_seq: u64,\n 90|    /// The prior generation's wall-clock start (ignored as authoritative on\n*91|    /// handoff — kept only to demonstrate it is *not* carried forward).\n 92|    pub gen_start_ms: u64,\n 93|}\n 94|\n...\n 204|/// channel supplies the deadline. On a timeout the pump bubbles out to a\n*205|/// supervised restart (a fresh [`Brain`] → fresh split → fresh reader thread);\n 206|/// the old reader, parked in `read_frame`, is abandoned (KH 7.6 leak watch — the\n 207|/// old conn's drop eventually errors its read and the thread exits).\n 208|enum BrainConn {\n...\n 216|        /// The reader-thread handle — held so the thread is owned, not detached\n*217|        /// silently. On a supervised restart the whole [`Brain`] drops; this\n 218|        /// handle drops with it (the thread, blocked in `read_frame`, is\n 219|        /// abandoned and exits when the old conn closes — KH 7.6).\n 220|        _reader: JoinHandle<()>,\n...\n 272|    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.\n*273|    /// Durability across a handoff is the logic layer's job (a re-attaching\n 274|    /// brain queries `net-streams` and re-subscribes from its durable cursors).\n 275|    net_cursors: std::collections::HashMap<u64, u64>,\n 276|    /// The presence resume cursor (D4c) — the conn-liveness sibling of\n...\n 283|    /// [`Brain::resume_sessions`], replacing the retired single-session\n*284|    /// `BrainState` handoff. A session present here is in **resume mode** — its\n 285|    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n 286|    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n 287|    /// boundary re-send) and accept-and-snap `seq >= cursor` (the contiguous case\n...\n 367|    ///\n*368|    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n*369|    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n 370|    /// integration tests) replays output through it with no later `attach`. So it\n 371|    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n 372|    /// ALSO subscribes from 0 over this same socket → two racing\n...\n 381|    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n*382|    /// never this brain→brain frame; `handoff` survives only for the integration\n 383|    /// tests. See [`BrainState`].\n 384|    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n*385|    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n 386|        let mut conn = LocalSocketTransport::connect(name)?;\n 387|        send_hello(&mut conn, Role::Brain)?;\n 388|        // DEFENSE-IN-DEPTH (P1c): seed the dedup cursor at the resume point so\n...\n 415|\n*416|    /// The generation counter (0 on cold start, +1 per handoff).\n 417|    pub fn generation(&self) -> u64 {\n 418|        self.generation\n 419|    }\n...\n 430|\n*431|    /// Snapshot durable state for a future handoff. **Test-only (restoration\n 432|    /// D4-3)** — the production path reconstructs from the broker, never a\n 433|    /// snapshot frame; see [`BrainState`].\n 434|    pub fn snapshot(&self) -> BrainState {\n### brainproc.rs\n*1|//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n 2|//! broker/brain process-isolation restoration, D1 skeleton).\n 3|//!\n 4|//! ADR-0004 specified the broker/brain split as two **processes**; production\n...\n 7|//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n*8|//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n 9|//! `spt daemon brain` child. The child connects over the existing broker socket\n 10|//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n 11|//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n...\n 18|//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n*19|//! broker spawns + supervises a brain child over the socket, and the broker\n 20|//! (with its PTY children + seed channel) survives the brain dying — the\n*21|//! supervisor respawns it from the executable path, which on a real update is\n 22|//! the swapped binary.\n 23|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 24|\n...\n 37|/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n*38|/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n 39|/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\n 40|const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n 41|/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n...\n 51|/// (a transient first-boot hiccup should not abandon a good binary) before the\n*52|/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n 53|/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n 54|/// window-expiry triggers rollback.\n 55|const TRIAL_PREREADY_BUDGET: u32 = 3;\n...\n 61|const BRAIN_CONNECT_TRIES: u32 = 300;\n*62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n 63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n 64|\n 65|/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n...\n 74|#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n*75|pub enum StartReason {\n 76|    /// The broker's first brain spawn this broker lifetime.\n 77|    Cold,\n*78|    /// A supervised respawn after an unexpected brain exit.\n 79|    Crash,\n 80|    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n 81|    Update,\n...\n 83|\n*84|impl StartReason {\n 85|    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n 86|    pub fn as_arg(self) -> &'static str {\n 87|        match self {\n*88|            StartReason::Cold => \"cold\",\n*89|            StartReason::Crash => \"crash\",\n*90|            StartReason::Update => \"update\",\n 91|        }\n 92|    }\n 93|\n...\n 98|        match s {\n*99|            \"update\" => StartReason::Update,\n*100|            \"crash\" => StartReason::Crash,\n*101|            _ => StartReason::Cold,\n 102|        }\n 103|    }\n 104|}\n 105|\n*106|/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n*107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n 108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n*110|/// between the broker serve path and the supervisor — both run in the broker\n 111|/// process. This is the seam that makes a routine `apply` actually run new code,\n 112|/// the pillar the in-process-collapse regression silently unrealized.\n 113|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n...\n 130|\n*131|    /// Consume a pending request (supervisor side): true at most once per\n 132|    /// `request`, clearing it so the next cycle starts fresh.\n 133|    pub fn take(&self) -> bool {\n 134|        self.requested.swap(false, Ordering::SeqCst)\n### broker.rs\n 9|//!\n*10|//! ## Gapless handoff (B2 — the load-bearing invariant)\n 11|//!\n 12|//! A brain may be killed and restarted at any moment (a routine self-update, a\n 13|//! crash). The broker — and every hosted child — must survive that untouched,\n...\n 145|\n*146|/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n 147|/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n 148|/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n 149|/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n...\n 335|/// shared with the inject worker and RESET to 0 on a healthy `{commit}`, so a\n*336|/// respawned binary that recovers refreshes the whole budget (the C3(b) give-up-\n 337|/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n 338|/// psyche crash-loop backoff's bounded give-up.\n 339|const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n...\n 502|    by: Option<String>,\n*503|    /// Time of the last SUCCESSFUL handoff (`try_send` Ok) to this controller's\n 504|    /// writer channel (b4, REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE).\n 505|    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n 506|    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n...\n 516|    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n*517|    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n 518|    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n 519|    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n 520|    /// `last_ok` stale. `Arc<Mutex<…>>` so the reader observes the writer's live\n...\n 726|    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n*727|    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n 728|    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n 729|    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n 730|    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n*731|    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n 732|    /// (bounded wedge preserved). The writer still does the blocking socket write +\n 733|    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n 734|    /// single drain thread.\n...\n 824|        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n*825|        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n 826|        // slow controller falls behind the ring) unless it has been Full for the\n 827|        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n 828|        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n...\n 1507|// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n*1508|#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n 1509|fn controller_writer(\n 1510|    sid: u64,\n 1511|    send: SharedSend,\n...\n 1603|            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n*1604|            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n 1605|            // a seq (dropped while Full). The frame still went to the socket (the live\n 1606|            // operator sees it), but the durable resume cursor must NOT jump past the\n 1607|            // gap — see [`contiguous_advance`].\n...\n 1678|    /// increments it per rebuild, the worker RESETS it to 0 on a healthy `{commit}`\n*1679|    /// (so a recovered binary refreshes the whole budget). At `translate_respawn_\n 1680|    /// budget()` the daemon gives up (loud) and leaves the binary dead → spool.\n 1681|    translation_respawns: Arc<AtomicU32>,\n 1682|    /// The adapter name owning this session (W3a, ADR-0025) — empty for a\n...\n 1948|            thread::spawn(move || {\n*1949|                run_inject_worker_supervised(\n 1950|                    sup_child,\n 1951|                    sup_faulted,\n 1952|                    sup_endpoint,\n...\n 2008|// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\n*2009|fn run_inject_worker_supervised(\n 2010|    child: Arc<TranslationChild>,\n 2011|    faulted: Arc<AtomicBool>,\n 2012|    endpoint: String,\n...\n 2375|        if committed {\n*2376|            // Healthy sequence: reset the strike counter, refresh the respawn budget\n 2377|            // (a working binary heals it), and forget any earlier respool of THIS\n 2378|            // envelope (it succeeded → a future miss may respool it afresh).\n 2379|            strikes = 0;\n...\n 2540|    net: std::sync::OnceLock<NetHost>,\n*2541|    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542|    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543|    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n*2544|    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n*2545|    /// `Daemon::run` after it spawns the supervisor.\n 2546|    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,\n 2547|    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a\n 2548|    /// `session_id → (subnet, real address, 5-min deadline)` between\n...\n 2703|\n*2704|    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n 2705|    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n*2706|    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707|    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n### config.rs\n 27|\n*28|/// Default orphan-watch grace window (B7): how long the supervised-crash teardown\n 29|/// waits after observing a hosted Self's harness owner gone before it rechecks\n 30|/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n*31|/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n 32|/// teardown for long. Configurable per machine via `crash_grace_ms`.\n 33|pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n 34|\n...\n 137|    /// The orphan-watch grace window (B7): the recovery wait before a\n*138|    /// supervised-crash teardown rechecks liveness and signs off (1.1).\n 139|    pub crash_grace: Duration,\n 140|    /// Whether self-update is full-auto (C2, REQ-UPD-4). **Default `false`** — an\n 141|    /// update is gated on user confirmation delivered to the most-recently-active\n### daemon.rs\n 185|        let net_up = net.is_some();\n*186|        // Keep the broker handle alive past the bind so the brain supervisor's\n 187|        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188|        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189|        let broker_handle: Option<Arc<Broker>> =\n 190|            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191|                Ok(broker) => {\n...\n 311|\n*312|        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n 313|        // restoration): the broker is the always-up per-machine anchor; it spawns\n*314|        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\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...\n 322|        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n*323|        // container the supervisor enrolls every (re)spawned brain into. Created\n*324|        // here so it outlives the supervisor and is reachable from the\n 325|        // graceful-stop path below. The brain subtree (the brain + its shellwake\n 326|        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n 327|        // never in the container.\n...\n 329|        let reaper = crate::reap::BrainReaper::new();\n*330|        // The supervisor stop flag — hoisted out of the wiring block (was held\n 331|        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n*332|        // it, so the supervisor does not respawn the brain we are about to reap.\n 333|        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n 334|        {\n*335|            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n 336|                Arc::clone(&brain_stop),\n 337|                Arc::clone(&reaper),\n 338|                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n...\n 358|        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n*359|        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n 360|        // does not respawn the brain we are about to kill) THEN reap the brain\n 361|        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n 362|        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n...\n 371|    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n*372|    /// broker process spawns + supervises this; it connects to the broker over\n 373|    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n 374|    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n 375|    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n*376|    /// is parsed leniently ([`crate::brainproc::StartReason::from_arg`]) so an\n 377|    /// unknown value from a newer broker defaults to `Cold` (KH-2.3).\n 378|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 379|    pub fn run_brain(generation: u64, start_reason: &str) -> io::Result<()> {\n...\n 390|            generation,\n*391|            crate::brainproc::StartReason::from_arg(start_reason),\n 392|        )\n 393|    }\n 394|}\n...\n 430|\n*431|/// Background net self-heal supervisor (REQ-DAEMON-9): a daemon that came up\n 432|/// net-less (NET_BIND_FAIL at boot — typically the autostart racing the network\n 433|/// up) keeps retrying `try_start_net` with capped backoff and, on success,\n 434|/// ATTACHES the host to the live broker (`OnceLock`, so exactly once). The node\n### deadline.rs\n 3|//! Phase-significant periodic timing (the pulse loop) and one-shot deadlines\n*4|//! (alarms) must survive a brain swap: the supervisor respawns a fresh brain\n 5|//! (`Brain::cold_start`) and the outgoing brain is gone before the new one starts\n 6|//! (D3-3), so continuity cannot ride a brain→brain frame — it must come from the\n 7|//! persistent side (disk), exactly as D4 moved session continuity to the broker.\n...\n 13|//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n*14|//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n*15|//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n 16|//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n*17|//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n 18|//!   IPC round-trip.\n 19|//!\n 20|//! - [`OneShotDeadline`] — an **absolute** `target_ms` fired once. Persisted at\n 21|//!   creation; **every** start reads it and fires-if-due; it is **never reset on\n*22|//!   any** `StartReason` (\"remind me at 3pm\" is a commitment that outlives any\n 23|//!   restart — the asymmetry vs the periodic crash-reset IS the rule). Built as a\n 24|//!   tested-but-unwired **pure helper** (ADR-0018 V3): the daemon has no one-shot\n 25|//!   consumer today (alarm is a legacy-listener in-memory timer,\n...\n 38|\n*39|use crate::brainproc::StartReason;\n 40|use crate::config::DEFAULT_PULSE_PERIOD;\n 41|\n 42|/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n...\n 77|\n*78|    /// Load-or-initialize the anchor for `key`, applying the [`StartReason`]\n 79|    /// rehydration rule (ADR-0018 Q4):\n 80|    ///\n*81|    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n*82|    ///   only the interval refreshes from live config. If no file exists (updating\n 83|    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n 84|    ///   — never fail an update on a missing file.\n*85|    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n 86|    ///   at `now_ms` (phase reset acceptable; cold is the first-ever start).\n 87|    ///\n 88|    /// A corrupt file is treated as absent (degrade-to-fresh), matching\n...\n 94|        interval_ms: u64,\n*95|        reason: StartReason,\n 96|        now_ms: u64,\n 97|    ) -> std::io::Result<Self> {\n 98|        let path = anchor_path(key);\n...\n 100|        let anchor = match (reason, Self::load(&path)) {\n*101|            // Update keeps the persisted phase; the interval refreshes to config.\n*102|            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103|                anchor_ms: prev.anchor_ms,\n 104|                interval_ms,\n 105|            },\n...\n 180|/// **rule** as a tested-but-unwired pure helper: persist `target_ms` at creation,\n*181|/// fire-if-due on every start, **never reset** on any [`StartReason`]. No\n 182|/// scheduler/timer/thread lives here; the durable in-daemon alarm scheduler that\n 183|/// will drive it is the alarm-port deferral (`docs/DEFERRED.md`). [`fire_if_due`]\n*184|/// takes **no** `StartReason` — that absence IS the never-reset rule, made\n 185|/// explicit in code so the alarm port cannot re-derive the crash-asymmetry wrong.\n 186|///\n 187|/// [`fire_if_due`]: OneShotDeadline::fire_if_due\n...\n 303|        with_home(|_| {\n*304|            let cold = DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 305|            assert_eq!(cold.anchor_ms, 1_000);\n 306|            // A crash start rewrites the anchor to the restart instant (phase reset).\n*307|            let crash = DeadlineAnchor::open(\"pulse\", 100, StartReason::Crash, 9_999).unwrap();\n 308|            assert_eq!(crash.anchor_ms, 9_999);\n 309|        });\n 310|    }\n...\n 312|    #[test] // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*313|    fn update_keeps_anchor_and_refreshes_interval() {\n 314|        with_home(|_| {\n*315|            DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 316|            // Update preserves phase (anchor) but the interval tracks live config.\n*317|            let upd = DeadlineAnchor::open(\"pulse\", 250, StartReason::Update, 9_999).unwrap();\n 318|            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n### digest.rs\n 117|/// `from` points at the changed tail (a minimal delta); a window slide evicts the\n*118|/// oldest turn → the heads differ from index 0 → `from == 0` (a full refresh).\n 119|pub fn common_prefix_len(a: &[Turn], b: &[Turn]) -> usize {\n 120|    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()\n 121|}\n...\n 531|    // [unit->REQ-TERM-4] the diff base: a shared head yields its length (a tail\n*532|    // delta); a divergent front yields 0 (a full refresh).\n 533|    #[test]\n 534|    fn common_prefix_len_finds_the_unchanged_head() {\n 535|        let a = vec![turn(\"a\"), turn(\"b\")];\n### digesthub.rs\n 121|        // The changed turns begin past the equal-by-value head; a window slide\n*122|        // (front eviction) diverges at 0 → a full refresh.\n 123|        let from = common_prefix_len(&st.snapshot.turns, &new.turns);\n 124|        st.version += 1;\n 125|        let update = DigestUpdate {\n...\n 463|    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a\n*464|    // tail delta (from past the unchanged head); a window slide refreshes from 0.\n 465|    #[test]\n*466|    fn publish_delta_is_tail_then_full_refresh() {\n 467|        let hub = DigestHub::new();\n 468|        let (_b, _v, rx) = hub.subscribe(\"doyle\");\n 469|        hub.project_and_publish(\"doyle\", digest(vec![turn(\"a\"), turn(\"b\")]));\n...\n 477|        assert_eq!(up.turns[0].input.as_deref(), Some(\"c\"));\n*478|        // A front eviction (window slide) diverges at 0 → full refresh.\n 479|        hub.project_and_publish(\"doyle\", digest(vec![turn(\"b\"), turn(\"c\"), turn(\"d\")]));\n 480|        let up = rx.recv().unwrap();\n 481|        assert_eq!(up.from, 0, \"front eviction resends from 0\");\n### endpoint.rs\n 35|\n*36|/// The broker listener name (B1/B2): PTY hosting + gapless handoff.\n 37|pub fn broker_socket_name() -> String {\n 38|    format!(\"spt-daemon-broker-{}.sock\", home_tag())\n 39|}\n...\n 75|/// child's pid, rewritten each heartbeat). The broker spawns the brain as a\n*76|/// supervised child process (ADR-0018 Q2/Q3 — the two-process restoration); the\n*77|/// child writes this once it has connected to the broker socket, and refreshes\n 78|/// it while attached. A test (or a future readiness-gated rollback, ADR-0018\n 79|/// Q7) reads it to confirm the brain came up and — on a pid change — that it\n 80|/// respawned. Best-effort like the daemon pid breadcrumb; liveness of the\n### lib.rs\n*1|//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).\n 2|//!\n 3|//! ADR-0004 splits the logical daemon into two implementation layers so a\n 4|//! routine self-update swaps the logic without disturbing live endpoints:\n...\n 45|//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).\n*46|//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →\n 47|//! drop → re-attach — so the live PTY child + its output stream survive the logic\n*48|//! swap untouched; it never re-implements the handoff and never touches a broker\n 49|//! resource.\n 50|//!\n*51|//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a\n 52|//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback\n 53|//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key\n 54|//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a\n...\n 97|//!\n*98|//! The brain + gapless handoff (B2) and the rest of the M3b task table build on\n*99|//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK\n 100|//! (R-ARCH-2).\n 101|//!\n 102|//! [`Envelope`]: frame::Envelope\n...\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### lifecycle.rs\n 35|\n*36|use crate::brainproc::StartReason;\n 37|use crate::config::DaemonConfig;\n 38|use crate::deadline::DeadlineAnchor;\n 39|use crate::notif::NotifSurfacePolicy;\n...\n 307|/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n*308|/// **refresh** them while the brain driver thread keeps running. The\n 309|/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n 310|/// otherwise unreachable from an IPC apply handler — so ADR-0025's \"re-clone the\n 311|/// manifest into the running BrainLifecycle\" is realized as a clonable handle the\n...\n 331|    /// never holds the read lock across the spawn/blocking call — a concurrent\n*332|    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n 333|    fn runtime_snapshot(&self) -> ManifestRuntime {\n 334|        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n 335|    }\n...\n 342|    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n*343|    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n 344|    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n 345|    /// brain thread, PTY, broker session, and output log are untouched; only these\n 346|    /// two cells' contents change. (W3c)\n 347|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n*348|    pub fn refresh(&self, new: Manifest, install_dir: Option<&Path>) {\n 349|        let runtime = build_runtime(&new, install_dir);\n 350|        // Write the runtime first, then the manifest: a reader that snapshots\n 351|        // between the two writes sees the new runtime with the old manifest at\n...\n 371|pub struct BrainLifecycle {\n*372|    /// Manifest + runtime behind a refreshable shared handle (W3c).\n 373|    cell: ManifestCell,\n 374|    /// The adapter install dir, retained for the W3c reload-on-dirty poll\n*375|    /// (`<install_dir>/manifest.toml`) and to re-pin the runtime on refresh.\n 376|    /// `None` ⇒ PATH-only resolution + no live-reload (a test/legacy host).\n 377|    install_dir: Option<PathBuf>,\n 378|    id: String,\n...\n 437|        // psyche role's bare program token resolves there before PATH (a\n*438|        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n 439|        Some(BrainLifecycle {\n 440|            cell: ManifestCell::new(manifest.clone(), install_dir),\n 441|            install_dir: install_dir.map(Path::to_path_buf),\n...\n 450|\n*451|    /// Refresh this endpoint's manifest + runtime in place — a thin delegate to\n 452|    /// the shared cell. The production trigger is [`reload_manifest_if_changed`]\n 453|    /// (the brain pulling an on-disk swap); this direct entry exists for tests and\n 454|    /// any in-process caller. Brain-parity: nothing else cycles. (W3c)\n 455|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n*456|    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {\n*457|        self.cell.refresh(new, install_dir);\n 458|    }\n 459|\n 460|    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\n...\n 470|    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\n*471|    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\n 472|    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\n 473|    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\n 474|    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\n...\n 498|            Some(new) => {\n*499|                self.cell.refresh(new, Some(dir));\n 500|                *last = Some(hash);\n 501|            }\n 502|            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\n...\n 581|\n*582|    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n 583|    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n 584|    /// echo-commune (T3).\n 585|    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n...\n 587|        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n*588|        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n 589|        // `last_active_ms` — a resting seat ticking its pulse must never win\n 590|        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n 591|        // the notif first-fire). A dormant seat instead checks the opt-in\n...\n 682|    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\n*683|    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\n 684|    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\n 685|    /// deadline is derived functionally — **no per-fire write**.\n 686|    ///\n...\n 695|        stop: &AtomicBool,\n*696|        reason: StartReason,\n 697|        mut on_tick: F,\n 698|    ) {\n 699|        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\n...\n 714|        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n*715|        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n 716|        // the initial state.\n 717|        let mut manifest_hash = self.manifest_disk_hash();\n 718|        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n...\n 755|        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n*756|        // history fetch below), so a live-update refresh never waits on it.\n 757|        let manifest = self.cell.manifest_snapshot();\n 758|        let Some(h) = &manifest.history else {\n 759|            return Vec::new();\n...\n 1213|    /// the Self is leaving on purpose, there is nothing to recover. The\n*1214|    /// supervised-crash path ([`supervised_crash_signoff`](Self::supervised_crash_signoff))\n 1215|    /// supplies the real grace + liveness recheck for an *un*intentional death.\n 1216|    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {\n 1217|        self.signoff_with(session_id, Duration::ZERO, || true)\n### livehost.rs\n 5|//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n*6|//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n 7|//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n 8|//! disk): same shape — enumerate Self perches, resolve each manifest from its\n*9|//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run\n 10|//! one bounded reconcile sweep at boot and every tick. Where the shell waker\n 11|//! hosts **offline** shells, this hosts **online** live endpoints — `status` is\n 12|//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the\n...\n 38|use crate::brain::{now_ms, Brain};\n*39|use crate::brainproc::StartReason;\n 40|use crate::config::DaemonConfig;\n 41|use crate::lifecycle::BrainLifecycle;\n 42|\n...\n 150|    cfg: &DaemonConfig,\n*151|    reason: StartReason,\n 152|) {\n 153|    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n 154|    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n...\n 613|/// daemon online-stamp makes liveness daemon-authoritative), then start the\n*614|/// config-paced pulse driver on its own supervised thread with a held stop flag.\n 615|/// A Psyche-spawn failure tables nothing — the next sweep retries.\n 616|// [impl->REQ-DAEMON-1]\n 617|fn host_one(\n...\n 621|    session_id: String,\n*622|    reason: StartReason,\n 623|) {\n 624|    lifecycle.sweep_on_start();\n 625|    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n...\n 977|// [impl->REQ-DAEMON-1]\n*978|pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n 979|    std::thread::spawn(move || {\n 980|        let set = Arc::new(LiveSet::new());\n 981|        {\n...\n 1316|                &cfg,\n*1317|                StartReason::Cold,\n 1318|            );\n 1319|\n 1320|            let status = |id: &str| {\n...\n 1391|                &fast_cfg(),\n*1392|                StartReason::Cold,\n 1393|            );\n 1394|\n 1395|            let exists = |wid: &str| {\n...\n 1573|                &cfg,\n*1574|                StartReason::Cold,\n 1575|            );\n 1576|            assert_eq!(set.len(), 1, \"the online live endpoint is hosted\");\n 1577|\n...\n 1608|                &fast_cfg(),\n*1609|                StartReason::Cold,\n 1610|            );\n 1611|            assert!(set.is_empty(), \"an offline live endpoint is never hosted\");\n 1612|        });\n...\n 1633|                    &cfg,\n*1634|                    StartReason::Cold,\n 1635|                )\n 1636|            };\n 1637|            run();\n...\n 1664|                &cfg,\n*1665|                StartReason::Cold,\n 1666|            );\n 1667|            assert_eq!(first.len(), 1);\n 1668|            // Brain \"restart\": drop the table (threads die with the process), the\n...\n 1680|                &cfg,\n*1681|                StartReason::Crash,\n 1682|            );\n 1683|            assert_eq!(second.len(), 1, \"a fresh brain re-hosts the online endpoint\");\n 1684|            set_then_teardown(&second);\n...\n 1719|            };\n*1720|            run(StartReason::Cold);\n 1721|            assert_eq!(set.len(), 1, \"hosted while online\");\n 1722|\n 1723|            // Graceful signoff flips the Self perch offline WHILE the driver pulses.\n...\n 1726|\n*1727|            run(StartReason::Cold);\n 1728|            assert!(\n 1729|                set.is_empty(),\n 1730|                \"an offline-transitioned endpoint is un-hosted (driver stopped + de-tabled)\"\n...\n 1755|                    &cfg,\n*1756|                    StartReason::Cold,\n 1757|                )\n 1758|            };\n 1759|            run();\n...\n 1991|                &fast_cfg(),\n*1992|                StartReason::Cold,\n 1993|            );\n 1994|            let perch = perch::resolve_perch_path(\"cold\", ParentHint::Infer);\n 1995|            let info = spt_store::info::read_info(&perch).unwrap();\n...\n 2028|                &fast_cfg(),\n*2029|                StartReason::Cold,\n 2030|            );\n 2031|            let info = spt_store::info::read_info(&perch).unwrap();\n 2032|            assert!(info.host_error.is_none(), \"no material → benign, not an error\");\n...\n 2052|                &fast_cfg(),\n*2053|                StartReason::Cold,\n 2054|            );\n 2055|            let info = spt_store::info::read_info(&perch).unwrap();\n 2056|            assert!(info.host_error.is_none(), \"not woken → no attempt, no error\");\n### msg.rs\n 226|/// Q3). `spt update apply` sends this after swapping the binary on disk; the\n*227|/// broker signals its brain supervisor to cycle the brain child with\n 228|/// `start-reason=update`, so the live brain restarts onto the new binary while\n 229|/// every broker-held endpoint stays untouched at the process level. Additive\n 230|/// verb (KH-2.3): an older broker that predates it answers with a generic error,\n...\n 233|/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n*234|/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235|/// acceptance, not completion. `honored` is false when the broker has no\n*236|/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237|/// reached a broker that cannot itself respawn.\n 238|pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239|\n...\n 872|/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n*873|/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874|/// cycle the child; false when the broker has none wired (a bare test broker),\n 875|/// so the caller can tell a real planned restart from a mere accept.\n 876|#[derive(Debug, Clone, Serialize, Deserialize)]\n### nethost.rs\n 68|/// continues, heartbeat advances), NOT the brain's own read-deadline (the A-half\n*69|/// poison → supervised-restart path). The 20s margin below 30s covers the error\n 70|/// frame's IPC transit + the brain's read scheduling. Test-overridable via\n 71|/// [`NetHost::set_quic_op_timeout`] so the hermetic black-hole tests fail fast.\n 72|const BROKER_QUIC_OP_TIMEOUT_MS: u64 = 10_000;\n...\n 988|    /// round continues, heartbeat advances) — DISTINCT from the brain's OWN\n*989|    /// read-deadline `TimedOut` (the A-half poison → supervised-restart signal).\n 990|    /// Because the bound fires (10s) well before the brain's 30s\n 991|    /// `PUMP_PEER_IO_TIMEOUT`, the broker always REPLIES first. A live op\n 992|    /// completes with zero added latency; the bound only bites a non-responsive\n### notif.rs\n 57|/// Producer kind for the auto-rollback alert (ADR-0018 Q7, restoration D6-2): a\n*58|/// brain-only update failed readiness and the supervisor rolled back to the\n 59|/// last-known-good binary. A distinct kind so a surface can style it; it rides\n 60|/// the same first-fire + **boundary-resurface** path as every other producer, so\n 61|/// an undismissed rollback row re-surfaces until the operator dismisses it\n...\n 426|/// Q7): a brain-only update `v{quarantine_version}` failed to reach `ready` and\n*427|/// the supervisor rolled back to `v{running_version}`, quarantining the bad\n 428|/// version. Reuses the one producer front door ([`produce_and_first_fire`]) so\n 429|/// the row rides first-fire AND **boundary-resurface** — an undismissed rollback\n 430|/// row re-surfaces at every boundary until the operator dismisses it (the\n 431|/// \"loud, not fire-once\" requirement; no new persistence path). The broker's\n*432|/// supervisor calls this once per member subnet on rollback.\n 433|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 434|#[allow(clippy::too_many_arguments)]\n 435|pub fn produce_rollback_notif(\n### pairhost.rs\n 76|    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);\n*77|    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every\n 78|    // served subnet so the joiner always learns at least us, then hand it the\n 79|    // whole current roster in the Seed frame.\n 80|    let mut roster = RosterStore::load();\n*81|    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());\n 82|    match tokio::time::timeout(\n 83|        CEREMONY_TIMEOUT,\n 84|        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),\n...\n 401|    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so\n*402|    // refresh covers it.\n 403|    // [impl->REQ-MESH-2]\n*404|    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());\n 405|    roster\n 406|        .save()\n 407|        .map_err(|e| JoinFail::Other(format!(\"roster save: {e}\")))?;\n...\n 475|        start,\n*476|        spt_net::net::pairing::ntp::force_refresh,\n 477|        Duration::from_millis(500),\n 478|    )\n 479|    .await\n...\n 495|/// query) and try ONE final probe before surrendering with the exhaustion\n*496|/// error. Generic over the probe + hook so the retry-once-after-refresh\n 497|/// behavior is unit-tested without a live `NetEndpoint`.\n 498|// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 499|async fn sweep_then_final_retry<A, E, P, Fut>(\n...\n 647|// [impl->REQ-MESH-2]\n*648|fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {\n 649|    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();\n 650|    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();\n 651|    let lease = spt_store::epoch::EpochSource::load().current();\n...\n 827|    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3 meet self-heal: on search-\n*828|    // deadline exhaustion the sweep forces ONE ceremony-clock refresh and tries\n*829|    // a final probe — proven here by a probe that FAILS until the refresh hook\n*830|    // fires, then succeeds. The final post-refresh sweep is what lands (the\n 831|    // stepped-clock join heals without a daemon bounce).\n 832|    #[tokio::test]\n*833|    async fn meet_retries_once_after_refresh_on_exhaustion() {\n 834|        use std::cell::Cell;\n*835|        let refreshed = Cell::new(false);","truncated":true,"truncatedBy":"bytes","totalLines":1143,"totalBytes":61621,"outputLines":928,"outputBytes":51169,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":1143,"totalBytes":61621,"outputLines":928,"outputBytes":51169,"shownRange":{"start":1,"end":928},"nextOffset":929}}},"isError":false,"timestamp":1784195748380}}
{"type":"message","id":"39868304","parentId":"6abbe046","timestamp":"2026-07-16T09:55:48.383Z","message":{"role":"toolResult","toolCallId":"call_ws6K4CS1GwrZ01qCkLARfXZY|fc_0cc7a0c9a797f232016a58aaa44bdc819ab2ac37cd913be987","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n\n### src/\n#### applyhost.rs\n 11|//!\n*12|//! [`apply_staged`] drives the whole chain against what the peer pump left\n 13|//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):\n 14|//!\n 15|//! 1. **Re-verify at rest** (`plan_verified` under the production\n...\n 51|\n*52|/// What [`apply_staged`] did — loud + typed, never silent.\n 53|#[derive(Debug, Clone, PartialEq, Eq)]\n 54|pub enum ApplyStagedOutcome {\n 55|    /// Nothing staged (or a torn stage that never offers) — idempotent no-op.\n...\n 105|/// swap, carrying the verified bytes. The shared verify/classify preamble both\n*106|/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on\n 107|/// whether a live broker is handed off to.\n 108|enum Prepared {\n 109|    /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /\n...\n 298|/// the canonical broker socket the brain handoff runs against. For a stopped box,\n*299|/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n 301|// [impl->REQ-UPD-3]\n 302|// [impl->REQ-UPD-4]\n*303|pub fn apply_staged(\n 304|    cache: &ReleaseCache,\n 305|    release_keys: &Path,\n 306|    exe_path: &Path,\n...\n 380|/// the handoff path's does. Runs the identical verify/classify gate as\n*381|/// [`apply_staged`], so every fail-closed refusal is preserved.\n 382|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n 383|// [impl->REQ-UPD-4]\n*384|pub fn apply_staged_daemonless(\n 385|    cache: &ReleaseCache,\n 386|    release_keys: &Path,\n 387|    exe_path: &Path,\n...\n 404|/// Already-applied classifier (F-025): whether the staged `staged_version` is\n*405|/// already the running/applied image, so `apply_staged` must short-circuit BEFORE\n 406|/// the binary-aside rename (re-renaming an already-swapped exe is today's os-error-5)\n 407|/// and report a friendly exit-0 no-op. Mirrors fetch's v0.18.0 classifier\n 408|/// (REQ-UPDATE-FETCH-CURRENT-UX): already-applied when the promoted applied version\n...\n 738|    #[test]\n*739|    fn apply_staged_refuses_platform_blind_or_mismatched_single() {\n 740|        let dir = tempfile::tempdir().unwrap();\n 741|        let name = unique_name();\n 742|        let _broker = served_broker(&name);\n...\n 751|        cache.stage(&signed, b\"CROSS-OS BYTES\").unwrap();\n*752|        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n 753|        assert!(\n 754|            matches!(out, ApplyStagedOutcome::Rejected(ref r) if r.contains(\"platform\")),\n 755|            \"unstamped single must refuse, got {out:?}\"\n...\n 770|            .unwrap();\n*771|        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n 772|        assert!(\n 773|            matches!(out, ApplyStagedOutcome::Rejected(ref r) if r.contains(other)),\n 774|            \"mismatched-platform single must refuse, got {out:?}\"\n...\n 782|            .unwrap();\n*783|        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n 784|        assert!(\n 785|            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n 786|            \"matching-platform single applies, got {out:?}\"\n...\n 877|    #[test]\n*878|    fn apply_staged_swaps_binary_and_hands_off_without_touching_the_endpoint() {\n 879|        let dir = tempfile::tempdir().unwrap();\n 880|        let name = unique_name();\n 881|        let broker = served_broker(&name);\n...\n 892|\n*893|        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"apply ok\");\n 894|        assert!(\n 895|            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n 896|            \"got {out:?}\"\n...\n 918|    // running AT ALL (none bound, none served — the stopped-box shape the wart\n*919|    // used to boot the old broker for), apply_staged_daemonless still swaps the\n 920|    // binary + records the open trial and returns AppliedDaemonless. RED without\n*921|    // the daemonless path: apply_staged would block on connect_retry against a\n 922|    // dead socket (300×10ms) and then fail — never landing the bytes. Proves the\n 923|    // swap no longer depends on a live broker to hand off to.\n 924|    #[test]\n*925|    fn apply_staged_daemonless_swaps_binary_with_no_broker() {\n 926|        let dir = tempfile::tempdir().unwrap();\n 927|        let exe = dir.path().join(\"spt-binary\");\n 928|        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n...\n 931|        // No served_broker — deliberately. A daemonless apply must not need one.\n*932|        let out = apply_staged_daemonless(&cache, &keys, &exe).expect(\"apply ok\");\n 933|        assert!(\n 934|            matches!(out, ApplyStagedOutcome::AppliedDaemonless { version: 7, .. }),\n 935|            \"got {out:?}\"\n...\n 963|    #[test]\n*964|    fn apply_staged_daemonless_holds_the_fail_closed_gates() {\n 965|        let dir = tempfile::tempdir().unwrap();\n 966|        let exe = dir.path().join(\"spt-binary\");\n 967|        std::fs::write(&exe, b\"OLD\").unwrap();\n...\n 971|        let keys = dir.path().join(\"release-keys.json\");\n*972|        let out = apply_staged_daemonless(&empty, &keys, &exe).expect(\"ok\");\n 973|        assert!(matches!(out, ApplyStagedOutcome::NoUpdate), \"got {out:?}\");\n 974|\n 975|        // Broker-breaking class: typed refusal, artifact stays staged, seat untouched.\n#### lib.rs\n 107|pub mod adapter_update;\n*108|pub mod applyhost;\n 109|pub mod attach;\n 110|pub mod autostart;\n 111|pub mod brain;\n...\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#### relcache.rs\n 43|/// (the v0.3.1 cross-OS brick root cause), so the apply guard\n*44|/// ([`crate::applyhost::apply_staged`]) trusts this stamp only when a path that\n 45|/// genuinely KNOWS the artifact's platform wrote it ([`stage_single_with_platform`]).\n 46|/// A plain [`stage`](ReleaseCache::stage) leaves it absent ⇒ the apply\n 47|/// fail-safe refuses (REQ-UPD-8). Update sets self-describe their platforms in\n#### release.rs\n 205|/// platform fact (`KNOWN_TARGET_TRIPLES`, the `current_platform()`\n*206|/// registry-membership invariant, applyhost's cross-platform \"other\" selection,\n 207|/// xtask's asset-name↔triple map) derives from [`SUPPORTED_PLATFORMS`]. Adding a\n 208|/// platform (musl, a future arm64) is one row here — \"structurally easy\"\n 209|/// (CONTEXT.md), not a scattered edit across ~6 sites.\n#### update.rs\n 241|///\n*242|/// The production caller is [`crate::applyhost::apply_staged`] (M5-D7b): the\n 243|/// peer pump still stops at *staged + consent-notified* (REQ-UPD-4 gated\n 244|/// default), and the explicit user ack (`spt update apply`) drives the\n 245|/// re-verify + class-check + brain handoff orchestration through this seam.\n\n### tests/\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,\n\n## spt-net/src/net/\n### replicate.rs\n 139|// [impl->REQ-INST-7]\n*140|pub fn apply_update(\n 141|    registries: &mut BTreeMap<String, SubnetRegistry>,\n 142|    update: RegistryUpdate,\n 143|) -> Option<MergeOutcome> {\n...\n 232|\n*233|        let out = apply_update(&mut regs, upd(\"home\", \"doyle\", \"n1\", Status::Active, 3));\n 234|        assert_eq!(out, Some(MergeOutcome::Inserted));\n 235|        assert_eq!(regs[\"home\"].instances(\"doyle\").len(), 1);\n 236|\n*237|        let dropped = apply_update(\n 238|            &mut regs,\n 239|            upd(\"elsewhere\", \"doyle\", \"n1\", Status::Active, 3),\n 240|        );\n...\n 298|        assert_eq!(\n*299|            apply_update(&mut regs, upd(\"home\", \"doyle\", \"n1\", Status::Offline, 9)),\n 300|            Some(MergeOutcome::Inserted)\n 301|        );\n 302|        assert_eq!(\n*303|            apply_update(&mut regs, upd(\"home\", \"doyle\", \"n1\", Status::Active, 5)),\n 304|            Some(MergeOutcome::Stale),\n 305|            \"lagging Active dropped\"\n 306|        );\n\n## spt/\n\n### src/\n#### cli.rs\n 1443|        } => match action {\n*1444|            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n 1445|            Some(UpdateCmd::Fetch {\n 1446|                channel,\n 1447|                tag,\n 1448|                apply,\n*1449|            }) => cmd_update_fetch(channel, tag, apply),\n 1450|            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n 1451|            Some(UpdateCmd::Adapters { names }) => {\n 1452|                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n...\n 1455|            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n*1456|            None => cmd_update_composite(core_only, restart),\n 1457|        },\n 1458|        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n 1459|        Cmd::Install { dir, no_path } => cmd_install(dir, no_path),\n...\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...\n 4911|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n*4912|fn cmd_update_apply(finish: bool) -> i32 {\n 4913|    let exe = match std::env::current_exe() {\n 4914|        Ok(p) => p,\n 4915|        Err(e) => {\n...\n 4923|    if finish {\n*4924|        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n 4925|        if code == 0 {\n 4926|            land_staged_docs(&cache);\n 4927|        }\n...\n 4935|    let outcome = if spt_daemon::is_running() {\n*4936|        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\n 4937|    } else {\n*4938|        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\n 4939|    };\n 4940|    match outcome {\n 4941|        Ok(o) => {\n...\n 4987|    } else {\n*4988|        UpdateLeg::FetchApply\n 4989|    });\n 4990|    if !core_only {\n 4991|        legs.push(UpdateLeg::Adapters);\n...\n 5002|/// core leg by construction — apply cycles the BRAIN only, the broker and every\n*5003|/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n 5004|/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n 5005|/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n 5006|///\n...\n 5016|// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n*5017|fn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n 5018|    let mut worst = 0;\n 5019|    for leg in plan_update_legs(core_only, restart) {\n 5020|        let code = match leg {\n*5021|            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\n*5022|            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\n 5023|            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\n*5024|            UpdateLeg::Finish => cmd_update_apply(true),\n 5025|        };\n 5026|        if code != 0 {\n 5027|            if composite_abort_on_failure(leg) {\n...\n 5045|fn composite_abort_on_failure(leg: UpdateLeg) -> bool {\n*5046|    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\n 5047|}\n 5048|\n 5049|/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\n...\n 5145|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n*5146|fn cmd_update_apply_finish(\n 5147|    cache: &spt_daemon::ReleaseCache,\n 5148|    keys: &std::path::Path,\n 5149|    exe: &std::path::Path,\n...\n 5154|    //    a running old broker paired with new brain bytes.\n*5155|    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\n 5156|        Ok(O::NoUpdate) => {\n 5157|            println!(\"NO_UPDATE\");\n 5158|            return 0;\n...\n 6109|\n*6110|fn cmd_update_fetch(channel: Option<String>, tag: Option<String>, apply: bool) -> i32 {\n 6111|    let repo = install_repo();\n 6112|\n 6113|    // The gh CLI is the mandated carrier (ADR-0036 §3): the channel is private,\n...\n 6201|            return match fetch_reject_action(class, apply) {\n*6202|                FetchAction::Apply => cmd_update_apply(false),\n 6203|                FetchAction::DoneOk => {\n 6204|                    match class {\n 6205|                        FetchRejectClass::AlreadyStaged { candidate } => println!(\n...\n 6330|            if apply {\n*6331|                return cmd_update_apply(false);\n 6332|            }\n 6333|            0\n 6334|        }\n\n### tests/\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|//!"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":53,"fileCount":9,"files":["crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/release.rs","crates/spt-daemon/src/update.rs","crates/spt-daemon/tests/replicate.rs","crates/spt-net/src/net/replicate.rs","crates/spt/src/cli.rs","crates/spt/tests/daemon_refresh_e2e.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":20},{"path":"crates/spt-daemon/src/lib.rs","count":2},{"path":"crates/spt-daemon/src/relcache.rs","count":1},{"path":"crates/spt-daemon/src/release.rs","count":1},{"path":"crates/spt-daemon/src/update.rs","count":1},{"path":"crates/spt-daemon/tests/replicate.rs","count":2},{"path":"crates/spt-net/src/net/replicate.rs","count":5},{"path":"crates/spt/src/cli.rs","count":20},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/\n\n### src/\n#### applyhost.rs\n  11│//!\n *12│//! [`apply_staged`] drives the whole chain against what the peer pump left\n  13│//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):\n  14│//!\n  15│//! 1. **Re-verify at rest** (`plan_verified` under the production\n    │...\n  51│\n *52│/// What [`apply_staged`] did — loud + typed, never silent.\n  53│#[derive(Debug, Clone, PartialEq, Eq)]\n  54│pub enum ApplyStagedOutcome {\n  55│    /// Nothing staged (or a torn stage that never offers) — idempotent no-op.\n    │...\n 105│/// swap, carrying the verified bytes. The shared verify/classify preamble both\n*106│/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on\n 107│/// whether a live broker is handed off to.\n 108│enum Prepared {\n 109│    /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /\n    │...\n 298│/// the canonical broker socket the brain handoff runs against. For a stopped box,\n*299│/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300│/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n 301│// [impl->REQ-UPD-3]\n 302│// [impl->REQ-UPD-4]\n*303│pub fn apply_staged(\n 304│    cache: &ReleaseCache,\n 305│    release_keys: &Path,\n 306│    exe_path: &Path,\n    │...\n 380│/// the handoff path's does. Runs the identical verify/classify gate as\n*381│/// [`apply_staged`], so every fail-closed refusal is preserved.\n 382│// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n 383│// [impl->REQ-UPD-4]\n*384│pub fn apply_staged_daemonless(\n 385│    cache: &ReleaseCache,\n 386│    release_keys: &Path,\n 387│    exe_path: &Path,\n    │...\n 404│/// Already-applied classifier (F-025): whether the staged `staged_version` is\n*405│/// already the running/applied image, so `apply_staged` must short-circuit BEFORE\n 406│/// the binary-aside rename (re-renaming an already-swapped exe is today's os-error-5)\n 407│/// and report a friendly exit-0 no-op. Mirrors fetch's v0.18.0 classifier\n 408│/// (REQ-UPDATE-FETCH-CURRENT-UX): already-applied when the promoted applied version\n    │...\n 738│    #[test]\n*739│    fn apply_staged_refuses_platform_blind_or_mismatched_single() {\n 740│        let dir = tempfile::tempdir().unwrap();\n 741│        let name = unique_name();\n 742│        let _broker = served_broker(&name);\n    │...\n 751│        cache.stage(&signed, b\"CROSS-OS BYTES\").unwrap();\n*752│        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n 753│        assert!(\n 754│            matches!(out, ApplyStagedOutcome::Rejected(ref r) if r.contains(\"platform\")),\n 755│            \"unstamped single must refuse, got {out:?}\"\n    │...\n 770│            .unwrap();\n*771│        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n 772│        assert!(\n 773│            matches!(out, ApplyStagedOutcome::Rejected(ref r) if r.contains(other)),\n 774│            \"mismatched-platform single must refuse, got {out:?}\"\n    │...\n 782│            .unwrap();\n*783│        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"ok\");\n 784│        assert!(\n 785│            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n 786│            \"matching-platform single applies, got {out:?}\"\n    │...\n 877│    #[test]\n*878│    fn apply_staged_swaps_binary_and_hands_off_without_touching_the_endpoint() {\n 879│        let dir = tempfile::tempdir().unwrap();\n 880│        let name = unique_name();\n 881│        let broker = served_broker(&name);\n    │...\n 892│\n*893│        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"apply ok\");\n 894│        assert!(\n 895│            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n 896│            \"got {out:?}\"\n    │...\n 918│    // running AT ALL (none bound, none served — the stopped-box shape the wart\n*919│    // used to boot the old broker for), apply_staged_daemonless still swaps the\n 920│    // binary + records the open trial and returns AppliedDaemonless. RED without\n*921│    // the daemonless path: apply_staged would block on connect_retry against a\n 922│    // dead socket (300×10ms) and then fail — never landing the bytes. Proves the\n 923│    // swap no longer depends on a live broker to hand off to.\n 924│    #[test]\n*925│    fn apply_staged_daemonless_swaps_binary_with_no_broker() {\n 926│        let dir = tempfile::tempdir().unwrap();\n 927│        let exe = dir.path().join(\"spt-binary\");\n 928│        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n    │...\n 931│        // No served_broker — deliberately. A daemonless apply must not need one.\n*932│        let out = apply_staged_daemonless(&cache, &keys, &exe).expect(\"apply ok\");\n 933│        assert!(\n 934│            matches!(out, ApplyStagedOutcome::AppliedDaemonless { version: 7, .. }),\n 935│            \"got {out:?}\"\n    │...\n 963│    #[test]\n*964│    fn apply_staged_daemonless_holds_the_fail_closed_gates() {\n 965│        let dir = tempfile::tempdir().unwrap();\n 966│        let exe = dir.path().join(\"spt-binary\");\n 967│        std::fs::write(&exe, b\"OLD\").unwrap();\n    │...\n 971│        let keys = dir.path().join(\"release-keys.json\");\n*972│        let out = apply_staged_daemonless(&empty, &keys, &exe).expect(\"ok\");\n 973│        assert!(matches!(out, ApplyStagedOutcome::NoUpdate), \"got {out:?}\");\n 974│\n 975│        // Broker-breaking class: typed refusal, artifact stays staged, seat untouched.\n#### lib.rs\n 107│pub mod adapter_update;\n*108│pub mod applyhost;\n 109│pub mod attach;\n 110│pub mod autostart;\n 111│pub mod brain;\n    │...\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#### relcache.rs\n 43│/// (the v0.3.1 cross-OS brick root cause), so the apply guard\n*44│/// ([`crate::applyhost::apply_staged`]) trusts this stamp only when a path that\n 45│/// genuinely KNOWS the artifact's platform wrote it ([`stage_single_with_platform`]).\n 46│/// A plain [`stage`](ReleaseCache::stage) leaves it absent ⇒ the apply\n 47│/// fail-safe refuses (REQ-UPD-8). Update sets self-describe their platforms in\n#### release.rs\n 205│/// platform fact (`KNOWN_TARGET_TRIPLES`, the `current_platform()`\n*206│/// registry-membership invariant, applyhost's cross-platform \"other\" selection,\n 207│/// xtask's asset-name↔triple map) derives from [`SUPPORTED_PLATFORMS`]. Adding a\n 208│/// platform (musl, a future arm64) is one row here — \"structurally easy\"\n 209│/// (CONTEXT.md), not a scattered edit across ~6 sites.\n#### update.rs\n 241│///\n*242│/// The production caller is [`crate::applyhost::apply_staged`] (M5-D7b): the\n 243│/// peer pump still stops at *staged + consent-notified* (REQ-UPD-4 gated\n 244│/// default), and the explicit user ack (`spt update apply`) drives the\n 245│/// re-verify + class-check + brain handoff orchestration through this seam.\n\n### tests/\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,\n\n## spt-net/src/net/\n### replicate.rs\n 139│// [impl->REQ-INST-7]\n*140│pub fn apply_update(\n 141│    registries: &mut BTreeMap<String, SubnetRegistry>,\n 142│    update: RegistryUpdate,\n 143│) -> Option<MergeOutcome> {\n    │...\n 232│\n*233│        let out = apply_update(&mut regs, upd(\"home\", \"doyle\", \"n1\", Status::Active, 3));\n 234│        assert_eq!(out, Some(MergeOutcome::Inserted));\n 235│        assert_eq!(regs[\"home\"].instances(\"doyle\").len(), 1);\n 236│\n*237│        let dropped = apply_update(\n 238│            &mut regs,\n 239│            upd(\"elsewhere\", \"doyle\", \"n1\", Status::Active, 3),\n 240│        );\n    │...\n 298│        assert_eq!(\n*299│            apply_update(&mut regs, upd(\"home\", \"doyle\", \"n1\", Status::Offline, 9)),\n 300│            Some(MergeOutcome::Inserted)\n 301│        );\n 302│        assert_eq!(\n*303│            apply_update(&mut regs, upd(\"home\", \"doyle\", \"n1\", Status::Active, 5)),\n 304│            Some(MergeOutcome::Stale),\n 305│            \"lagging Active dropped\"\n 306│        );\n\n## spt/\n\n### src/\n#### cli.rs\n 1443│        } => match action {\n*1444│            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n 1445│            Some(UpdateCmd::Fetch {\n 1446│                channel,\n 1447│                tag,\n 1448│                apply,\n*1449│            }) => cmd_update_fetch(channel, tag, apply),\n 1450│            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n 1451│            Some(UpdateCmd::Adapters { names }) => {\n 1452│                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n     │...\n 1455│            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n*1456│            None => cmd_update_composite(core_only, restart),\n 1457│        },\n 1458│        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n 1459│        Cmd::Install { dir, no_path } => cmd_install(dir, no_path),\n     │...\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     │...\n 4911│// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n*4912│fn cmd_update_apply(finish: bool) -> i32 {\n 4913│    let exe = match std::env::current_exe() {\n 4914│        Ok(p) => p,\n 4915│        Err(e) => {\n     │...\n 4923│    if finish {\n*4924│        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n 4925│        if code == 0 {\n 4926│            land_staged_docs(&cache);\n 4927│        }\n     │...\n 4935│    let outcome = if spt_daemon::is_running() {\n*4936│        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\n 4937│    } else {\n*4938│        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\n 4939│    };\n 4940│    match outcome {\n 4941│        Ok(o) => {\n     │...\n 4987│    } else {\n*4988│        UpdateLeg::FetchApply\n 4989│    });\n 4990│    if !core_only {\n 4991│        legs.push(UpdateLeg::Adapters);\n     │...\n 5002│/// core leg by construction — apply cycles the BRAIN only, the broker and every\n*5003│/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n 5004│/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n 5005│/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n 5006│///\n     │...\n 5016│// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n*5017│fn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n 5018│    let mut worst = 0;\n 5019│    for leg in plan_update_legs(core_only, restart) {\n 5020│        let code = match leg {\n*5021│            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\n*5022│            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\n 5023│            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\n*5024│            UpdateLeg::Finish => cmd_update_apply(true),\n 5025│        };\n 5026│        if code != 0 {\n 5027│            if composite_abort_on_failure(leg) {\n     │...\n 5045│fn composite_abort_on_failure(leg: UpdateLeg) -> bool {\n*5046│    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\n 5047│}\n 5048│\n 5049│/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\n     │...\n 5145│// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n*5146│fn cmd_update_apply_finish(\n 5147│    cache: &spt_daemon::ReleaseCache,\n 5148│    keys: &std::path::Path,\n 5149│    exe: &std::path::Path,\n     │...\n 5154│    //    a running old broker paired with new brain bytes.\n*5155│    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\n 5156│        Ok(O::NoUpdate) => {\n 5157│            println!(\"NO_UPDATE\");\n 5158│            return 0;\n     │...\n 6109│\n*6110│fn cmd_update_fetch(channel: Option<String>, tag: Option<String>, apply: bool) -> i32 {\n 6111│    let repo = install_repo();\n 6112│\n 6113│    // The gh CLI is the mandated carrier (ADR-0036 §3): the channel is private,\n     │...\n 6201│            return match fetch_reject_action(class, apply) {\n*6202│                FetchAction::Apply => cmd_update_apply(false),\n 6203│                FetchAction::DoneOk => {\n 6204│                    match class {\n 6205│                        FetchRejectClass::AlreadyStaged { candidate } => println!(\n     │...\n 6330│            if apply {\n*6331│                return cmd_update_apply(false);\n 6332│            }\n 6333│            0\n 6334│        }\n\n### tests/\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│//!"},"isError":false,"timestamp":1784195748383}}
{"type":"message","id":"b11551b2","parentId":"39868304","timestamp":"2026-07-16T09:55:48.388Z","message":{"role":"toolResult","toolCallId":"call_pUQqapKqql72Lk6PWCZZQS7C|fc_0cc7a0c9a797f232016a58aaa44bd0819a9c1931743a9a1e4e","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### applyhost.rs\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...\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...\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*833|        // No supervisor wired: the refresh round-trips and reports not-honored.\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*839|        // Supervisor wired: refresh raises the same planned-restart signal the\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 848|            signal.take(),\n*849|            \"refresh must raise the planned-restart signal the supervisor consumes\"\n 850|        );\n 851|    }\n 852|\n### brainproc.rs\n 61|const BRAIN_CONNECT_TRIES: u32 = 300;\n*62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n 63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n 64|\n 65|/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n### broker.rs\n 335|/// shared with the inject worker and RESET to 0 on a healthy `{commit}`, so a\n*336|/// respawned binary that recovers refreshes the whole budget (the C3(b) give-up-\n 337|/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n 338|/// psyche crash-loop backoff's bounded give-up.\n 339|const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n...\n 1678|    /// increments it per rebuild, the worker RESETS it to 0 on a healthy `{commit}`\n*1679|    /// (so a recovered binary refreshes the whole budget). At `translate_respawn_\n 1680|    /// budget()` the daemon gives up (loud) and leaves the binary dead → spool.\n 1681|    translation_respawns: Arc<AtomicU32>,\n 1682|    /// The adapter name owning this session (W3a, ADR-0025) — empty for a\n...\n 2375|        if committed {\n*2376|            // Healthy sequence: reset the strike counter, refresh the respawn budget\n 2377|            // (a working binary heals it), and forget any earlier respool of THIS\n 2378|            // envelope (it succeeded → a future miss may respool it afresh).\n 2379|            strikes = 0;\n### deadline.rs\n 81|    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n*82|    ///   only the interval refreshes from live config. If no file exists (updating\n 83|    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n 84|    ///   — never fail an update on a missing file.\n 85|    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n...\n 100|        let anchor = match (reason, Self::load(&path)) {\n*101|            // Update keeps the persisted phase; the interval refreshes to config.\n 102|            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103|                anchor_ms: prev.anchor_ms,\n 104|                interval_ms,\n...\n 312|    #[test] // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*313|    fn update_keeps_anchor_and_refreshes_interval() {\n 314|        with_home(|_| {\n 315|            DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 316|            // Update preserves phase (anchor) but the interval tracks live config.\n...\n 318|            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n*319|            assert_eq!(upd.interval_ms, 250, \"update must refresh the interval\");\n 320|        });\n 321|    }\n 322|\n### digest.rs\n 117|/// `from` points at the changed tail (a minimal delta); a window slide evicts the\n*118|/// oldest turn → the heads differ from index 0 → `from == 0` (a full refresh).\n 119|pub fn common_prefix_len(a: &[Turn], b: &[Turn]) -> usize {\n 120|    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()\n 121|}\n...\n 531|    // [unit->REQ-TERM-4] the diff base: a shared head yields its length (a tail\n*532|    // delta); a divergent front yields 0 (a full refresh).\n 533|    #[test]\n 534|    fn common_prefix_len_finds_the_unchanged_head() {\n 535|        let a = vec![turn(\"a\"), turn(\"b\")];\n### digesthub.rs\n 121|        // The changed turns begin past the equal-by-value head; a window slide\n*122|        // (front eviction) diverges at 0 → a full refresh.\n 123|        let from = common_prefix_len(&st.snapshot.turns, &new.turns);\n 124|        st.version += 1;\n 125|        let update = DigestUpdate {\n...\n 463|    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a\n*464|    // tail delta (from past the unchanged head); a window slide refreshes from 0.\n 465|    #[test]\n*466|    fn publish_delta_is_tail_then_full_refresh() {\n 467|        let hub = DigestHub::new();\n 468|        let (_b, _v, rx) = hub.subscribe(\"doyle\");\n 469|        hub.project_and_publish(\"doyle\", digest(vec![turn(\"a\"), turn(\"b\")]));\n...\n 477|        assert_eq!(up.turns[0].input.as_deref(), Some(\"c\"));\n*478|        // A front eviction (window slide) diverges at 0 → full refresh.\n 479|        hub.project_and_publish(\"doyle\", digest(vec![turn(\"b\"), turn(\"c\"), turn(\"d\")]));\n 480|        let up = rx.recv().unwrap();\n 481|        assert_eq!(up.from, 0, \"front eviction resends from 0\");\n### endpoint.rs\n 76|/// supervised child process (ADR-0018 Q2/Q3 — the two-process restoration); the\n*77|/// child writes this once it has connected to the broker socket, and refreshes\n 78|/// it while attached. A test (or a future readiness-gated rollback, ADR-0018\n 79|/// Q7) reads it to confirm the brain came up and — on a pid change — that it\n 80|/// respawned. Best-effort like the daemon pid breadcrumb; liveness of the\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### lifecycle.rs\n 307|/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n*308|/// **refresh** them while the brain driver thread keeps running. The\n 309|/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n 310|/// otherwise unreachable from an IPC apply handler — so ADR-0025's \"re-clone the\n 311|/// manifest into the running BrainLifecycle\" is realized as a clonable handle the\n...\n 331|    /// never holds the read lock across the spawn/blocking call — a concurrent\n*332|    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n 333|    fn runtime_snapshot(&self) -> ManifestRuntime {\n 334|        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n 335|    }\n...\n 342|    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n*343|    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n 344|    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n 345|    /// brain thread, PTY, broker session, and output log are untouched; only these\n 346|    /// two cells' contents change. (W3c)\n 347|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n*348|    pub fn refresh(&self, new: Manifest, install_dir: Option<&Path>) {\n 349|        let runtime = build_runtime(&new, install_dir);\n 350|        // Write the runtime first, then the manifest: a reader that snapshots\n 351|        // between the two writes sees the new runtime with the old manifest at\n...\n 371|pub struct BrainLifecycle {\n*372|    /// Manifest + runtime behind a refreshable shared handle (W3c).\n 373|    cell: ManifestCell,\n 374|    /// The adapter install dir, retained for the W3c reload-on-dirty poll\n*375|    /// (`<install_dir>/manifest.toml`) and to re-pin the runtime on refresh.\n 376|    /// `None` ⇒ PATH-only resolution + no live-reload (a test/legacy host).\n 377|    install_dir: Option<PathBuf>,\n 378|    id: String,\n...\n 437|        // psyche role's bare program token resolves there before PATH (a\n*438|        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n 439|        Some(BrainLifecycle {\n 440|            cell: ManifestCell::new(manifest.clone(), install_dir),\n 441|            install_dir: install_dir.map(Path::to_path_buf),\n...\n 450|\n*451|    /// Refresh this endpoint's manifest + runtime in place — a thin delegate to\n 452|    /// the shared cell. The production trigger is [`reload_manifest_if_changed`]\n 453|    /// (the brain pulling an on-disk swap); this direct entry exists for tests and\n 454|    /// any in-process caller. Brain-parity: nothing else cycles. (W3c)\n 455|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n*456|    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {\n*457|        self.cell.refresh(new, install_dir);\n 458|    }\n 459|\n 460|    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\n...\n 470|    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\n*471|    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\n 472|    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\n 473|    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\n 474|    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\n...\n 498|            Some(new) => {\n*499|                self.cell.refresh(new, Some(dir));\n 500|                *last = Some(hash);\n 501|            }\n 502|            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\n...\n 581|\n*582|    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n 583|    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n 584|    /// echo-commune (T3).\n 585|    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n...\n 587|        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n*588|        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n 589|        // `last_active_ms` — a resting seat ticking its pulse must never win\n 590|        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n 591|        // the notif first-fire). A dormant seat instead checks the opt-in\n...\n 714|        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n*715|        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n 716|        // the initial state.\n 717|        let mut manifest_hash = self.manifest_disk_hash();\n 718|        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n...\n 755|        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n*756|        // history fetch below), so a live-update refresh never waits on it.\n 757|        let manifest = self.cell.manifest_snapshot();\n 758|        let Some(h) = &manifest.history else {\n 759|            return Vec::new();\n...\n 2400|\n*2401|            // Dormant: the tick must NOT refresh recency — and with the node\n 2402|            // knob on + a stale dormancy anchor, it auto-suspends instead.\n 2403|            crate::resting::write_rest(&perch_path, crate::resting::RestState::Dormant, 1_000)\n 2404|                .unwrap();\n...\n 2651|\n*2652|    // --- W3c live-update: ManifestCell refresh + reload-on-dirty (REQ-ADAPTER-LIVE-UPDATE, ADR-0025) ---\n 2653|\n 2654|    // The TOML for a live-capable manifest at a given adapter.version. Same shape\n 2655|    // as `live_manifest` (Psyche init + commune dir) so it parses into a host;\n*2656|    // only `adapter.version` varies, giving an observable signal that a refresh /\n 2657|    // reload swapped the manifest (and the runtime built from it).\n 2658|    fn live_manifest_toml(commune_dir: &std::path::Path, version: &str) -> String {\n 2659|        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n...\n 2680|\n*2681|    // (1) refresh swaps BOTH the manifest cell and the runtime built from it: the\n### pairhost.rs\n 76|    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);\n*77|    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every\n 78|    // served subnet so the joiner always learns at least us, then hand it the\n 79|    // whole current roster in the Seed frame.\n 80|    let mut roster = RosterStore::load();\n*81|    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());\n 82|    match tokio::time::timeout(\n 83|        CEREMONY_TIMEOUT,\n 84|        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),\n...\n 401|    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so\n*402|    // refresh covers it.\n 403|    // [impl->REQ-MESH-2]\n*404|    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());\n 405|    roster\n 406|        .save()\n 407|        .map_err(|e| JoinFail::Other(format!(\"roster save: {e}\")))?;\n...\n 475|        start,\n*476|        spt_net::net::pairing::ntp::force_refresh,\n 477|        Duration::from_millis(500),\n 478|    )\n 479|    .await\n...\n 495|/// query) and try ONE final probe before surrendering with the exhaustion\n*496|/// error. Generic over the probe + hook so the retry-once-after-refresh\n 497|/// behavior is unit-tested without a live `NetEndpoint`.\n 498|// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 499|async fn sweep_then_final_retry<A, E, P, Fut>(\n...\n 647|// [impl->REQ-MESH-2]\n*648|fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {\n 649|    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();\n 650|    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();\n 651|    let lease = spt_store::epoch::EpochSource::load().current();\n...\n 827|    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3 meet self-heal: on search-\n*828|    // deadline exhaustion the sweep forces ONE ceremony-clock refresh and tries\n*829|    // a final probe — proven here by a probe that FAILS until the refresh hook\n*830|    // fires, then succeeds. The final post-refresh sweep is what lands (the\n 831|    // stepped-clock join heals without a daemon bounce).\n 832|    #[tokio::test]\n*833|    async fn meet_retries_once_after_refresh_on_exhaustion() {\n 834|        use std::cell::Cell;\n*835|        let refreshed = Cell::new(false);\n 836|        let calls = Cell::new(0u32);\n 837|        let probe = || async {\n 838|            calls.set(calls.get() + 1);\n*839|            if refreshed.get() {\n 840|                Ok(7u32)\n 841|            } else {\n 842|                Err(\"no seed-holder\")\n...\n 850|            start,\n*851|            || refreshed.set(true),\n 852|            Duration::from_millis(1),\n 853|        )\n 854|        .await;\n*855|        assert_eq!(got, Ok(7u32), \"the post-refresh final sweep lands\");\n*856|        assert_eq!(calls.get(), 2, \"one exhausting probe + one post-refresh retry\");\n*857|        assert!(refreshed.get(), \"the refresh hook fired between the two probes\");\n 858|    }\n 859|\n*860|    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] and if the final post-refresh sweep\n 861|    // ALSO fails, surrender with the richer exhaustion error — exactly ONE\n 862|    // retry, never an unbounded loop.\n 863|    #[tokio::test]\n### projwriter.rs\n 18|//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n*19|//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n 20|//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n 21|//!   scan;\n 22|//! - maintains an observability sidecar (`project-index-stats.json`) — index\n 23|//!   presence alone is not health: generated time, source generation, pending\n*24|//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n 25|//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n 26|//!   (wall-clock budgets are manual acceptance ONLY).\n 27|// [impl->REQ-PROJECT-INDEX-WRITER]\n...\n 90|    pub source_generation: String,\n*91|    /// Whether a refresh is queued/underway right now.\n 92|    #[serde(default)]\n*93|    pub pending_refresh: bool,\n 94|    /// When the last reconcile cycle ran (epoch ms).\n 95|    #[serde(default)]\n 96|    pub last_run_ms: u64,\n...\n 179|/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n*180|/// for a linked worktree). Identity refreshes ONLY when the marker path or\n 181|/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\n 182|/// §3: commits do not alter project identity).\n 183|#[derive(Debug, Clone)]\n...\n 245|            source_generation: last_fingerprint.clone(),\n*246|            pending_refresh: true, // the boot reconcile is queued by definition\n 247|            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n 248|            ..IndexWriterStats::default()\n 249|        };\n...\n 518|        self.stats.last_run_ms = now_ms();\n*519|        self.stats.pending_refresh = true; // the work is still owed\n 520|        self.write_stats();\n 521|    }\n 522|\n...\n 528|        self.stats.last_error = None;\n*529|        self.stats.pending_refresh = false;\n 530|        self.stats.cwds = self.cwd_cache.len() as u64;\n 531|        self.write_stats();\n 532|    }\n...\n 666|                // Debounce: let the burst land, then drain it ALL — the\n*667|                // coalescing window (multiple invalidations → one refresh).\n 668|                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n 669|                std::thread::sleep(debounce);\n*670|                engine.stats.pending_refresh = true;\n 671|                engine.write_stats();\n 672|            }\n 673|            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n...\n 894|        assert!(stats.last_error.is_some(), \"the failure is an observable fact\");\n*895|        assert!(stats.pending_refresh, \"the owed work stays visible\");\n 896|    }\n 897|\n 898|    // [unit->REQ-PROJECT-INDEX-INVALIDATION] scoped invalidation: an endpoint\n...\n 1080|            // Wait for the boot cycle to settle.\n*1081|            while stats().map(|s| s.pending_refresh || s.generated_ms == 0).unwrap_or(true) {\n 1082|                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n 1083|                std::thread::sleep(Duration::from_millis(25));\n 1084|            }\n...\n 1095|            while spt_store::projinval::pending_at(&spt_store::projinval::invalidations_dir())\n*1096|                || stats().map(|s| s.pending_refresh).unwrap_or(true)\n 1097|            {\n 1098|                assert!(Instant::now() < consumed, \"burst never consumed\");\n 1099|                std::thread::sleep(Duration::from_millis(25));\n...\n 1266|        assert_eq!(stats.endpoints, 1);\n*1267|        assert!(!stats.pending_refresh);\n 1268|    }\n 1269|}\n### registryhost.rs\n 526|        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\n*527|        // this scan only refreshes perches that still exist, so the stale row is\n 528|        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\n 529|        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\n 530|        // epoch: the lease orders it, so it stops showing Active locally\n...\n 1520|                row.epoch > before,\n*1521|                \"the refresh rides the lease like any update\"\n 1522|            );\n 1523|        });\n 1524|    }\n...\n 1990|\n*1991|        // A Stale verdict still refreshes the stamp (the sender is alive).\n 1992|        std::thread::sleep(std::time::Duration::from_millis(15));\n 1993|        let v = h.apply_feed(\"bb22\", &[upd(\"home\", \"ling\", \"bb22\", 5)], &p);\n 1994|        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);\n...\n 1998|            *heard.get(\"bb22\").unwrap() > first,\n*1999|            \"stale merge still refreshes recency\"\n 2000|        );\n 2001|    }\n 2002|\n### seedmap.rs\n 15|//! (ADR-0004 §B), not routing/registry/parse logic. It is intentionally tiny:\n*16|//! put / take by pid. Because seeds are ephemeral (refreshed on the next\n 17|//! SessionStart), a daemon restart dropping the map is recoverable — the harness\n 18|//! re-seeds.\n 19|//!\n...\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 84|\n*85|    /// Store (or refresh) a seed keyed by its `parent_pid`.\n 86|    pub fn put(&self, seed: Seed) {\n 87|        self.map.lock().unwrap().insert(seed.parent_pid, seed);\n 88|    }\n...\n 555|\n*556|    // [unit->REQ-START-3] a re-PUT for the same pid refreshes the stored seed.\n 557|    #[test]\n*558|    fn put_refreshes_existing() {\n 559|        with_home(|_| {\n 560|            let reg = Arc::new(SeedRegistry::new());\n 561|            let _srv = start(Arc::clone(&reg));\n### sync.rs\n 446|                            // none of it rides the ContextStore nudge funnel.\n*447|                            // Nudge ONE global refresh per pull that applied any\n 448|                            // p-* branch; the fingerprint gates what actually\n 449|                            // rescans, and the debounce coalesces a multi-peer\n 450|                            // pull burst. Without this, readers (index-only\n\n### pump/\n#### registry.rs\n 257|    #[test]\n*258|    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\n 259|        with_home(|home| {\n 260|            // A served subnet plus one advertisable Self perch.\n 261|            let owlery = home.join(\"owlery\");\n\n## spt-msg/src/\n### ready.rs\n 134|            );\n*135|            // New session/cwd → refresh this endpoint's project-index row\n 136|            // (+ the shared cwd cache) — ADR-0037 §3. Fire-and-forget.\n 137|            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n 138|            spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n\n## spt-net/src/net/\n### registry.rs\n 1796|            MergeOutcome::Updated,\n*1797|            \"a heartbeat refresh is an ordinary epoch-bumped update\"\n 1798|        );\n 1799|        assert_eq!(reg.instances(\"ling\")[0].last_active_ms, Some(2_000));\n 1800|        // A lagging row drops under the lease — recency never rolls back.\n\n### pairing/\n#### ntp.rs\n 28|//! - **D2 loud-fail** ([`ntp_transition`]): a total (all-servers-unreachable)\n*29|//!   refresh logs the transition instead of silently running on the raw clock.\n*30|//! - **D3 clock-step** ([`refresh_needed`]): the cache snapshots an\n*31|//!   `(Instant, SystemTime)` pair and force-refreshes when the wall clock\n 32|//!   STEPS under the daemon (a `w32tm /resync` yank), instead of applying an\n 33|//!   offset measured against the pre-step clock for the whole TTL.\n 34|// [impl->REQ-PAIR-8]\n...\n 67|/// is the OS-clock-step tell (D3). `offset_secs` is the last known correction\n*68|/// (`0` = system clock); `uncorrected` tracks whether the last refresh reached\n 69|/// NO server (the D2 transition state — never spams, logs only the edge).\n 70|struct CachedOffset {\n 71|    when: Option<Instant>,\n...\n 111|// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n*112|pub fn force_refresh() {\n 113|    let mut cache = OFFSET.lock().unwrap_or_else(|p| p.into_inner());\n 114|    invalidate(&mut cache);\n 115|}\n 116|\n*117|/// Drop the snapshot so [`refresh_needed`] reports stale on the next read.\n 118|// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 119|fn invalidate(cache: &mut CachedOffset) {\n 120|    cache.when = None;\n...\n 126|/// answered, so the ceremony ran on the raw system clock — the enlyzeam blind\n*127|/// spot). Refreshes the cache like any ceremony read; both facts read under one\n 128|/// lock so they cannot skew against each other.\n 129|// [impl->REQ-JOIN-VERBOSE-CLOCK]\n 130|pub fn ceremony_offset_state() -> (i64, bool) {\n*131|    let offset = current_offset_secs(); // refresh-if-stale + return offset\n 132|    let cache = OFFSET.lock().unwrap_or_else(|p| p.into_inner());\n 133|    (offset, !cache.uncorrected)\n 134|}\n 135|\n*136|/// The current in-process offset, refreshing the cache when stale. `0` when\n 137|/// NTP is disabled, unreachable, or simply agrees with the system clock.\n 138|// [impl->REQ-PAIR-NTP-LOUD-FAIL]\n 139|fn current_offset_secs() -> i64 {\n...\n 142|    let wall_now = SystemTime::now();\n*143|    if refresh_needed(cache.when, cache.wall_at, mono_now, wall_now, OFFSET_TTL) {\n 144|        let outcome = query_offset_secs();\n 145|        let (uncorrected, transition) = ntp_transition(cache.uncorrected, outcome);\n 146|        cache.uncorrected = uncorrected;\n...\n 174|// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n*175|fn refresh_needed(\n 176|    when: Option<Instant>,\n 177|    wall_at: Option<SystemTime>,\n 178|    mono_now: Instant,\n...\n 210|/// The D2 loud-fail state machine: given the prior uncorrected state and this\n*211|/// refresh's outcome (`None` = no server answered), yield the new uncorrected\n 212|/// state and which transition (if any) to log. LOGS ONLY THE EDGE — a run of\n*213|/// failures logs once, not per refresh.\n 214|// [impl->REQ-PAIR-NTP-LOUD-FAIL]\n 215|fn ntp_transition(prev_uncorrected: bool, outcome: Option<i64>) -> (bool, Transition) {\n 216|    match (prev_uncorrected, outcome) {\n...\n 223|\n*224|/// What (if anything) a refresh transition should log.\n 225|#[derive(Debug, PartialEq, Eq)]\n 226|enum Transition {\n 227|    Silent,\n...\n 273|/// before reaching a live A — the enlyzeam shape (4×AAAA ahead of the A) burned\n*274|/// ~6s per refresh. Stable within each family (a working server's own address\n 275|/// order is preserved).\n 276|// [impl->REQ-PAIR-NTP-MULTIHOME]\n 277|fn sort_v4_first(addrs: &mut [SocketAddr]) {\n...\n 458|    // [unit->REQ-PAIR-NTP-LOUD-FAIL] D2: the transition state machine logs\n*459|    // ONLY the edges — into all-failed once, recovery once — never per-refresh\n 460|    // spam while the failure (or the corrected steady state) persists.\n 461|    #[test]\n 462|    fn loud_fail_logs_only_on_transitions() {\n...\n 477|    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3: an OS clock STEP under the\n*478|    // daemon forces a refresh via injected clock reads — no real 15-min TTL\n 479|    // wait, no waiting for a real clock to jump. A wall step (forward OR back)\n 480|    // with the monotonic clock barely moved re-queries; matched wall/mono\n 481|    // within the TTL does not.\n 482|    #[test]\n*483|    fn clock_step_forces_refresh_via_injected_reads() {\n 484|        let mono0 = Instant::now();\n 485|        let wall0 = UNIX_EPOCH + Duration::from_secs(1_700_000_000);\n 486|        let ttl = Duration::from_secs(900);\n...\n 488|\n*489|        // Clocks agree (both +10s), within TTL: NO refresh.\n 490|        assert!(\n*491|            !refresh_needed(\n 492|                Some(mono0),\n 493|                Some(wall0),\n 494|                mono_now,\n\n## spt-runtime/src/\n### registry.rs\n 23|//! the adapter is quiesced — this module records state, it never execs.\n*24|//! Re-adding a deregistered name re-activates it (refreshing the record).\n 25|//!\n 26|//! Layout per adapter: `adapters/<name>/record.toml` (+ `manifest.toml` in\n 27|//! copy mode). Records write atomically (REQ-HAZARD-EBUSY-RENAME stance via\n...\n 295|/// the `[update]` avenue — `delegated` ⇒ pointer, `file_pull`/absent ⇒ copy.\n*296|/// Re-adding an existing name refreshes the record and re-activates a\n 297|/// soft-deregistered one. Returns the record; the caller conducts\n 298|/// install-is-first-update through the manifest's `[update]` avenue.\n 299|// [impl->REQ-INSTALL-4]\n...\n 1113|            re.active && re.registered_at_ms == 2000,\n*1114|            \"re-add re-activates + refreshes\"\n 1115|        );\n 1116|        assert_eq!(registered(&adapters).len(), 1);\n 1117|    }\n### runtime.rs\n 618|/// shared lock and spawn against the snapshot without holding the guard (W3c,\n*619|/// the live-update manifest-refresh handle).\n 620|#[derive(Clone)]\n 621|pub struct ManifestRuntime {\n 622|    manifest: Manifest,\n\nShowing files 1-20 of 33+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":114,"fileCount":20,"files":["crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/pairhost.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/sync.rs","crates/spt-msg/src/ready.rs","crates/spt-net/src/net/pairing/ntp.rs","crates/spt-net/src/net/registry.rs","crates/spt-runtime/src/registry.rs","crates/spt-runtime/src/runtime.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":9},{"path":"crates/spt-daemon/src/brainproc.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":3},{"path":"crates/spt-daemon/src/deadline.rs","count":4},{"path":"crates/spt-daemon/src/digest.rs","count":2},{"path":"crates/spt-daemon/src/digesthub.rs","count":4},{"path":"crates/spt-daemon/src/endpoint.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/pairhost.rs","count":18},{"path":"crates/spt-daemon/src/projwriter.rs","count":14},{"path":"crates/spt-daemon/src/pump/registry.rs","count":1},{"path":"crates/spt-daemon/src/registryhost.rs","count":4},{"path":"crates/spt-daemon/src/seedmap.rs","count":5},{"path":"crates/spt-daemon/src/sync.rs","count":1},{"path":"crates/spt-msg/src/ready.rs","count":1},{"path":"crates/spt-net/src/net/pairing/ntp.rs","count":20},{"path":"crates/spt-net/src/net/registry.rs","count":1},{"path":"crates/spt-runtime/src/registry.rs","count":3},{"path":"crates/spt-runtime/src/runtime.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/src/\n### applyhost.rs\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    │...\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    │...\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*833│        // No supervisor wired: the refresh round-trips and reports not-honored.\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*839│        // Supervisor wired: refresh raises the same planned-restart signal the\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 848│            signal.take(),\n*849│            \"refresh must raise the planned-restart signal the supervisor consumes\"\n 850│        );\n 851│    }\n 852│\n### brainproc.rs\n 61│const BRAIN_CONNECT_TRIES: u32 = 300;\n*62│/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n 63│const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n 64│\n 65│/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n### broker.rs\n  335│/// shared with the inject worker and RESET to 0 on a healthy `{commit}`, so a\n *336│/// respawned binary that recovers refreshes the whole budget (the C3(b) give-up-\n  337│/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n  338│/// psyche crash-loop backoff's bounded give-up.\n  339│const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n     │...\n 1678│    /// increments it per rebuild, the worker RESETS it to 0 on a healthy `{commit}`\n*1679│    /// (so a recovered binary refreshes the whole budget). At `translate_respawn_\n 1680│    /// budget()` the daemon gives up (loud) and leaves the binary dead → spool.\n 1681│    translation_respawns: Arc<AtomicU32>,\n 1682│    /// The adapter name owning this session (W3a, ADR-0025) — empty for a\n     │...\n 2375│        if committed {\n*2376│            // Healthy sequence: reset the strike counter, refresh the respawn budget\n 2377│            // (a working binary heals it), and forget any earlier respool of THIS\n 2378│            // envelope (it succeeded → a future miss may respool it afresh).\n 2379│            strikes = 0;\n### deadline.rs\n  81│    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n *82│    ///   only the interval refreshes from live config. If no file exists (updating\n  83│    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n  84│    ///   — never fail an update on a missing file.\n  85│    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n    │...\n 100│        let anchor = match (reason, Self::load(&path)) {\n*101│            // Update keeps the persisted phase; the interval refreshes to config.\n 102│            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103│                anchor_ms: prev.anchor_ms,\n 104│                interval_ms,\n    │...\n 312│    #[test] // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*313│    fn update_keeps_anchor_and_refreshes_interval() {\n 314│        with_home(|_| {\n 315│            DeadlineAnchor::open(\"pulse\", 100, StartReason::Cold, 1_000).unwrap();\n 316│            // Update preserves phase (anchor) but the interval tracks live config.\n    │...\n 318│            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n*319│            assert_eq!(upd.interval_ms, 250, \"update must refresh the interval\");\n 320│        });\n 321│    }\n 322│\n### digest.rs\n 117│/// `from` points at the changed tail (a minimal delta); a window slide evicts the\n*118│/// oldest turn → the heads differ from index 0 → `from == 0` (a full refresh).\n 119│pub fn common_prefix_len(a: &[Turn], b: &[Turn]) -> usize {\n 120│    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()\n 121│}\n    │...\n 531│    // [unit->REQ-TERM-4] the diff base: a shared head yields its length (a tail\n*532│    // delta); a divergent front yields 0 (a full refresh).\n 533│    #[test]\n 534│    fn common_prefix_len_finds_the_unchanged_head() {\n 535│        let a = vec![turn(\"a\"), turn(\"b\")];\n### digesthub.rs\n 121│        // The changed turns begin past the equal-by-value head; a window slide\n*122│        // (front eviction) diverges at 0 → a full refresh.\n 123│        let from = common_prefix_len(&st.snapshot.turns, &new.turns);\n 124│        st.version += 1;\n 125│        let update = DigestUpdate {\n    │...\n 463│    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a\n*464│    // tail delta (from past the unchanged head); a window slide refreshes from 0.\n 465│    #[test]\n*466│    fn publish_delta_is_tail_then_full_refresh() {\n 467│        let hub = DigestHub::new();\n 468│        let (_b, _v, rx) = hub.subscribe(\"doyle\");\n 469│        hub.project_and_publish(\"doyle\", digest(vec![turn(\"a\"), turn(\"b\")]));\n    │...\n 477│        assert_eq!(up.turns[0].input.as_deref(), Some(\"c\"));\n*478│        // A front eviction (window slide) diverges at 0 → full refresh.\n 479│        hub.project_and_publish(\"doyle\", digest(vec![turn(\"b\"), turn(\"c\"), turn(\"d\")]));\n 480│        let up = rx.recv().unwrap();\n 481│        assert_eq!(up.from, 0, \"front eviction resends from 0\");\n### endpoint.rs\n 76│/// supervised child process (ADR-0018 Q2/Q3 — the two-process restoration); the\n*77│/// child writes this once it has connected to the broker socket, and refreshes\n 78│/// it while attached. A test (or a future readiness-gated rollback, ADR-0018\n 79│/// Q7) reads it to confirm the brain came up and — on a pid change — that it\n 80│/// respawned. Best-effort like the daemon pid breadcrumb; liveness of the\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### lifecycle.rs\n  307│/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n *308│/// **refresh** them while the brain driver thread keeps running. The\n  309│/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n  310│/// otherwise unreachable from an IPC apply handler — so ADR-0025's \"re-clone the\n  311│/// manifest into the running BrainLifecycle\" is realized as a clonable handle the\n     │...\n  331│    /// never holds the read lock across the spawn/blocking call — a concurrent\n *332│    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n  333│    fn runtime_snapshot(&self) -> ManifestRuntime {\n  334│        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n  335│    }\n     │...\n  342│    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n *343│    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n  344│    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n  345│    /// brain thread, PTY, broker session, and output log are untouched; only these\n  346│    /// two cells' contents change. (W3c)\n  347│    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n *348│    pub fn refresh(&self, new: Manifest, install_dir: Option<&Path>) {\n  349│        let runtime = build_runtime(&new, install_dir);\n  350│        // Write the runtime first, then the manifest: a reader that snapshots\n  351│        // between the two writes sees the new runtime with the old manifest at\n     │...\n  371│pub struct BrainLifecycle {\n *372│    /// Manifest + runtime behind a refreshable shared handle (W3c).\n  373│    cell: ManifestCell,\n  374│    /// The adapter install dir, retained for the W3c reload-on-dirty poll\n *375│    /// (`<install_dir>/manifest.toml`) and to re-pin the runtime on refresh.\n  376│    /// `None` ⇒ PATH-only resolution + no live-reload (a test/legacy host).\n  377│    install_dir: Option<PathBuf>,\n  378│    id: String,\n     │...\n  437│        // psyche role's bare program token resolves there before PATH (a\n *438│        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n  439│        Some(BrainLifecycle {\n  440│            cell: ManifestCell::new(manifest.clone(), install_dir),\n  441│            install_dir: install_dir.map(Path::to_path_buf),\n     │...\n  450│\n *451│    /// Refresh this endpoint's manifest + runtime in place — a thin delegate to\n  452│    /// the shared cell. The production trigger is [`reload_manifest_if_changed`]\n  453│    /// (the brain pulling an on-disk swap); this direct entry exists for tests and\n  454│    /// any in-process caller. Brain-parity: nothing else cycles. (W3c)\n  455│    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n *456│    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {\n *457│        self.cell.refresh(new, install_dir);\n  458│    }\n  459│\n  460│    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\n     │...\n  470│    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\n *471│    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\n  472│    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\n  473│    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\n  474│    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\n     │...\n  498│            Some(new) => {\n *499│                self.cell.refresh(new, Some(dir));\n  500│                *last = Some(hash);\n  501│            }\n  502│            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\n     │...\n  581│\n *582│    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n  583│    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n  584│    /// echo-commune (T3).\n  585│    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n     │...\n  587│        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n *588│        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n  589│        // `last_active_ms` — a resting seat ticking its pulse must never win\n  590│        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n  591│        // the notif first-fire). A dormant seat instead checks the opt-in\n     │...\n  714│        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n *715│        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n  716│        // the initial state.\n  717│        let mut manifest_hash = self.manifest_disk_hash();\n  718│        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n     │...\n  755│        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n *756│        // history fetch below), so a live-update refresh never waits on it.\n  757│        let manifest = self.cell.manifest_snapshot();\n  758│        let Some(h) = &manifest.history else {\n  759│            return Vec::new();\n     │...\n 2400│\n*2401│            // Dormant: the tick must NOT refresh recency — and with the node\n 2402│            // knob on + a stale dormancy anchor, it auto-suspends instead.\n 2403│            crate::resting::write_rest(&perch_path, crate::resting::RestState::Dormant, 1_000)\n 2404│                .unwrap();\n     │...\n 2651│\n*2652│    // --- W3c live-update: ManifestCell refresh + reload-on-dirty (REQ-ADAPTER-LIVE-UPDATE, ADR-0025) ---\n 2653│\n 2654│    // The TOML for a live-capable manifest at a given adapter.version. Same shape\n 2655│    // as `live_manifest` (Psyche init + commune dir) so it parses into a host;\n*2656│    // only `adapter.version` varies, giving an observable signal that a refresh /\n 2657│    // reload swapped the manifest (and the runtime built from it).\n 2658│    fn live_manifest_toml(commune_dir: &std::path::Path, version: &str) -> String {\n 2659│        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n     │...\n 2680│\n*2681│    // (1) refresh swaps BOTH the manifest cell and the runtime built from it: the\n### pairhost.rs\n  76│    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);\n *77│    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every\n  78│    // served subnet so the joiner always learns at least us, then hand it the\n  79│    // whole current roster in the Seed frame.\n  80│    let mut roster = RosterStore::load();\n *81│    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());\n  82│    match tokio::time::timeout(\n  83│        CEREMONY_TIMEOUT,\n  84│        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),\n    │...\n 401│    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so\n*402│    // refresh covers it.\n 403│    // [impl->REQ-MESH-2]\n*404│    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());\n 405│    roster\n 406│        .save()\n 407│        .map_err(|e| JoinFail::Other(format!(\"roster save: {e}\")))?;\n    │...\n 475│        start,\n*476│        spt_net::net::pairing::ntp::force_refresh,\n 477│        Duration::from_millis(500),\n 478│    )\n 479│    .await\n    │...\n 495│/// query) and try ONE final probe before surrendering with the exhaustion\n*496│/// error. Generic over the probe + hook so the retry-once-after-refresh\n 497│/// behavior is unit-tested without a live `NetEndpoint`.\n 498│// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 499│async fn sweep_then_final_retry<A, E, P, Fut>(\n    │...\n 647│// [impl->REQ-MESH-2]\n*648│fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {\n 649│    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();\n 650│    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();\n 651│    let lease = spt_store::epoch::EpochSource::load().current();\n    │...\n 827│    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3 meet self-heal: on search-\n*828│    // deadline exhaustion the sweep forces ONE ceremony-clock refresh and tries\n*829│    // a final probe — proven here by a probe that FAILS until the refresh hook\n*830│    // fires, then succeeds. The final post-refresh sweep is what lands (the\n 831│    // stepped-clock join heals without a daemon bounce).\n 832│    #[tokio::test]\n*833│    async fn meet_retries_once_after_refresh_on_exhaustion() {\n 834│        use std::cell::Cell;\n*835│        let refreshed = Cell::new(false);\n 836│        let calls = Cell::new(0u32);\n 837│        let probe = || async {\n 838│            calls.set(calls.get() + 1);\n*839│            if refreshed.get() {\n 840│                Ok(7u32)\n 841│            } else {\n 842│                Err(\"no seed-holder\")\n    │...\n 850│            start,\n*851│            || refreshed.set(true),\n 852│            Duration::from_millis(1),\n 853│        )\n 854│        .await;\n*855│        assert_eq!(got, Ok(7u32), \"the post-refresh final sweep lands\");\n*856│        assert_eq!(calls.get(), 2, \"one exhausting probe + one post-refresh retry\");\n*857│        assert!(refreshed.get(), \"the refresh hook fired between the two probes\");\n 858│    }\n 859│\n*860│    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] and if the final post-refresh sweep\n 861│    // ALSO fails, surrender with the richer exhaustion error — exactly ONE\n 862│    // retry, never an unbounded loop.\n 863│    #[tokio::test]\n### projwriter.rs\n   18│//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n  *19│//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n   20│//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n   21│//!   scan;\n   22│//! - maintains an observability sidecar (`project-index-stats.json`) — index\n   23│//!   presence alone is not health: generated time, source generation, pending\n  *24│//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n   25│//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n   26│//!   (wall-clock budgets are manual acceptance ONLY).\n   27│// [impl->REQ-PROJECT-INDEX-WRITER]\n     │...\n   90│    pub source_generation: String,\n  *91│    /// Whether a refresh is queued/underway right now.\n   92│    #[serde(default)]\n  *93│    pub pending_refresh: bool,\n   94│    /// When the last reconcile cycle ran (epoch ms).\n   95│    #[serde(default)]\n   96│    pub last_run_ms: u64,\n     │...\n  179│/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n *180│/// for a linked worktree). Identity refreshes ONLY when the marker path or\n  181│/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\n  182│/// §3: commits do not alter project identity).\n  183│#[derive(Debug, Clone)]\n     │...\n  245│            source_generation: last_fingerprint.clone(),\n *246│            pending_refresh: true, // the boot reconcile is queued by definition\n  247│            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n  248│            ..IndexWriterStats::default()\n  249│        };\n     │...\n  518│        self.stats.last_run_ms = now_ms();\n *519│        self.stats.pending_refresh = true; // the work is still owed\n  520│        self.write_stats();\n  521│    }\n  522│\n     │...\n  528│        self.stats.last_error = None;\n *529│        self.stats.pending_refresh = false;\n  530│        self.stats.cwds = self.cwd_cache.len() as u64;\n  531│        self.write_stats();\n  532│    }\n     │...\n  666│                // Debounce: let the burst land, then drain it ALL — the\n *667│                // coalescing window (multiple invalidations → one refresh).\n  668│                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n  669│                std::thread::sleep(debounce);\n *670│                engine.stats.pending_refresh = true;\n  671│                engine.write_stats();\n  672│            }\n  673│            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n     │...\n  894│        assert!(stats.last_error.is_some(), \"the failure is an observable fact\");\n *895│        assert!(stats.pending_refresh, \"the owed work stays visible\");\n  896│    }\n  897│\n  898│    // [unit->REQ-PROJECT-INDEX-INVALIDATION] scoped invalidation: an endpoint\n     │...\n 1080│            // Wait for the boot cycle to settle.\n*1081│            while stats().map(|s| s.pending_refresh || s.generated_ms == 0).unwrap_or(true) {\n 1082│                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n 1083│                std::thread::sleep(Duration::from_millis(25));\n 1084│            }\n     │...\n 1095│            while spt_store::projinval::pending_at(&spt_store::projinval::invalidations_dir())\n*1096│                || stats().map(|s| s.pending_refresh).unwrap_or(true)\n 1097│            {\n 1098│                assert!(Instant::now() < consumed, \"burst never consumed\");\n 1099│                std::thread::sleep(Duration::from_millis(25));\n     │...\n 1266│        assert_eq!(stats.endpoints, 1);\n*1267│        assert!(!stats.pending_refresh);\n 1268│    }\n 1269│}\n### registryhost.rs\n  526│        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\n *527│        // this scan only refreshes perches that still exist, so the stale row is\n  528│        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\n  529│        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\n  530│        // epoch: the lease orders it, so it stops showing Active locally\n     │...\n 1520│                row.epoch > before,\n*1521│                \"the refresh rides the lease like any update\"\n 1522│            );\n 1523│        });\n 1524│    }\n     │...\n 1990│\n*1991│        // A Stale verdict still refreshes the stamp (the sender is alive).\n 1992│        std::thread::sleep(std::time::Duration::from_millis(15));\n 1993│        let v = h.apply_feed(\"bb22\", &[upd(\"home\", \"ling\", \"bb22\", 5)], &p);\n 1994│        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);\n     │...\n 1998│            *heard.get(\"bb22\").unwrap() > first,\n*1999│            \"stale merge still refreshes recency\"\n 2000│        );\n 2001│    }\n 2002│\n### seedmap.rs\n  15│//! (ADR-0004 §B), not routing/registry/parse logic. It is intentionally tiny:\n *16│//! put / take by pid. Because seeds are ephemeral (refreshed on the next\n  17│//! SessionStart), a daemon restart dropping the map is recoverable — the harness\n  18│//! re-seeds.\n  19│//!\n    │...\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  84│\n *85│    /// Store (or refresh) a seed keyed by its `parent_pid`.\n  86│    pub fn put(&self, seed: Seed) {\n  87│        self.map.lock().unwrap().insert(seed.parent_pid, seed);\n  88│    }\n    │...\n 555│\n*556│    // [unit->REQ-START-3] a re-PUT for the same pid refreshes the stored seed.\n 557│    #[test]\n*558│    fn put_refreshes_existing() {\n 559│        with_home(|_| {\n 560│            let reg = Arc::new(SeedRegistry::new());\n 561│            let _srv = start(Arc::clone(&reg));\n### sync.rs\n 446│                            // none of it rides the ContextStore nudge funnel.\n*447│                            // Nudge ONE global refresh per pull that applied any\n 448│                            // p-* branch; the fingerprint gates what actually\n 449│                            // rescans, and the debounce coalesces a multi-peer\n 450│                            // pull burst. Without this, readers (index-only\n\n### pump/\n#### registry.rs\n 257│    #[test]\n*258│    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\n 259│        with_home(|home| {\n 260│            // A served subnet plus one advertisable Self perch.\n 261│            let owlery = home.join(\"owlery\");\n\n## spt-msg/src/\n### ready.rs\n 134│            );\n*135│            // New session/cwd → refresh this endpoint's project-index row\n 136│            // (+ the shared cwd cache) — ADR-0037 §3. Fire-and-forget.\n 137│            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n 138│            spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n\n## spt-net/src/net/\n### registry.rs\n 1796│            MergeOutcome::Updated,\n*1797│            \"a heartbeat refresh is an ordinary epoch-bumped update\"\n 1798│        );\n 1799│        assert_eq!(reg.instances(\"ling\")[0].last_active_ms, Some(2_000));\n 1800│        // A lagging row drops under the lease — recency never rolls back.\n\n### pairing/\n#### ntp.rs\n  28│//! - **D2 loud-fail** ([`ntp_transition`]): a total (all-servers-unreachable)\n *29│//!   refresh logs the transition instead of silently running on the raw clock.\n *30│//! - **D3 clock-step** ([`refresh_needed`]): the cache snapshots an\n *31│//!   `(Instant, SystemTime)` pair and force-refreshes when the wall clock\n  32│//!   STEPS under the daemon (a `w32tm /resync` yank), instead of applying an\n  33│//!   offset measured against the pre-step clock for the whole TTL.\n  34│// [impl->REQ-PAIR-8]\n    │...\n  67│/// is the OS-clock-step tell (D3). `offset_secs` is the last known correction\n *68│/// (`0` = system clock); `uncorrected` tracks whether the last refresh reached\n  69│/// NO server (the D2 transition state — never spams, logs only the edge).\n  70│struct CachedOffset {\n  71│    when: Option<Instant>,\n    │...\n 111│// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n*112│pub fn force_refresh() {\n 113│    let mut cache = OFFSET.lock().unwrap_or_else(|p| p.into_inner());\n 114│    invalidate(&mut cache);\n 115│}\n 116│\n*117│/// Drop the snapshot so [`refresh_needed`] reports stale on the next read.\n 118│// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n 119│fn invalidate(cache: &mut CachedOffset) {\n 120│    cache.when = None;\n    │...\n 126│/// answered, so the ceremony ran on the raw system clock — the enlyzeam blind\n*127│/// spot). Refreshes the cache like any ceremony read; both facts read under one\n 128│/// lock so they cannot skew against each other.\n 129│// [impl->REQ-JOIN-VERBOSE-CLOCK]\n 130│pub fn ceremony_offset_state() -> (i64, bool) {\n*131│    let offset = current_offset_secs(); // refresh-if-stale + return offset\n 132│    let cache = OFFSET.lock().unwrap_or_else(|p| p.into_inner());\n 133│    (offset, !cache.uncorrected)\n 134│}\n 135│\n*136│/// The current in-process offset, refreshing the cache when stale. `0` when\n 137│/// NTP is disabled, unreachable, or simply agrees with the system clock.\n 138│// [impl->REQ-PAIR-NTP-LOUD-FAIL]\n 139│fn current_offset_secs() -> i64 {\n    │...\n 142│    let wall_now = SystemTime::now();\n*143│    if refresh_needed(cache.when, cache.wall_at, mono_now, wall_now, OFFSET_TTL) {\n 144│        let outcome = query_offset_secs();\n 145│        let (uncorrected, transition) = ntp_transition(cache.uncorrected, outcome);\n 146│        cache.uncorrected = uncorrected;\n    │...\n 174│// [impl->REQ-HAZARD-CEREMONY-CLOCK-STEP]\n*175│fn refresh_needed(\n 176│    when: Option<Instant>,\n 177│    wall_at: Option<SystemTime>,\n 178│    mono_now: Instant,\n    │...\n 210│/// The D2 loud-fail state machine: given the prior uncorrected state and this\n*211│/// refresh's outcome (`None` = no server answered), yield the new uncorrected\n 212│/// state and which transition (if any) to log. LOGS ONLY THE EDGE — a run of\n*213│/// failures logs once, not per refresh.\n 214│// [impl->REQ-PAIR-NTP-LOUD-FAIL]\n 215│fn ntp_transition(prev_uncorrected: bool, outcome: Option<i64>) -> (bool, Transition) {\n 216│    match (prev_uncorrected, outcome) {\n    │...\n 223│\n*224│/// What (if anything) a refresh transition should log.\n 225│#[derive(Debug, PartialEq, Eq)]\n 226│enum Transition {\n 227│    Silent,\n    │...\n 273│/// before reaching a live A — the enlyzeam shape (4×AAAA ahead of the A) burned\n*274│/// ~6s per refresh. Stable within each family (a working server's own address\n 275│/// order is preserved).\n 276│// [impl->REQ-PAIR-NTP-MULTIHOME]\n 277│fn sort_v4_first(addrs: &mut [SocketAddr]) {\n    │...\n 458│    // [unit->REQ-PAIR-NTP-LOUD-FAIL] D2: the transition state machine logs\n*459│    // ONLY the edges — into all-failed once, recovery once — never per-refresh\n 460│    // spam while the failure (or the corrected steady state) persists.\n 461│    #[test]\n 462│    fn loud_fail_logs_only_on_transitions() {\n    │...\n 477│    // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3: an OS clock STEP under the\n*478│    // daemon forces a refresh via injected clock reads — no real 15-min TTL\n 479│    // wait, no waiting for a real clock to jump. A wall step (forward OR back)\n 480│    // with the monotonic clock barely moved re-queries; matched wall/mono\n 481│    // within the TTL does not.\n 482│    #[test]\n*483│    fn clock_step_forces_refresh_via_injected_reads() {\n 484│        let mono0 = Instant::now();\n 485│        let wall0 = UNIX_EPOCH + Duration::from_secs(1_700_000_000);\n 486│        let ttl = Duration::from_secs(900);\n    │...\n 488│\n*489│        // Clocks agree (both +10s), within TTL: NO refresh.\n 490│        assert!(\n*491│            !refresh_needed(\n 492│                Some(mono0),\n 493│                Some(wall0),\n 494│                mono_now,\n\n## spt-runtime/src/\n### registry.rs\n   23│//! the adapter is quiesced — this module records state, it never execs.\n  *24│//! Re-adding a deregistered name re-activates it (refreshing the record).\n   25│//!\n   26│//! Layout per adapter: `adapters/<name>/record.toml` (+ `manifest.toml` in\n   27│//! copy mode). Records write atomically (REQ-HAZARD-EBUSY-RENAME stance via\n     │...\n  295│/// the `[update]` avenue — `delegated` ⇒ pointer, `file_pull`/absent ⇒ copy.\n *296│/// Re-adding an existing name refreshes the record and re-activates a\n  297│/// soft-deregistered one. Returns the record; the caller conducts\n  298│/// install-is-first-update through the manifest's `[update]` avenue.\n  299│// [impl->REQ-INSTALL-4]\n     │...\n 1113│            re.active && re.registered_at_ms == 2000,\n*1114│            \"re-add re-activates + refreshes\"\n 1115│        );\n 1116│        assert_eq!(registered(&adapters).len(), 1);\n 1117│    }\n### runtime.rs\n 618│/// shared lock and spawn against the snapshot without holding the guard (W3c,\n*619│/// the live-update manifest-refresh handle).\n 620│#[derive(Clone)]\n 621│pub struct ManifestRuntime {\n 622│    manifest: Manifest,"},"isError":false,"timestamp":1784195748388}}
{"type":"message","id":"453d20fb","parentId":"b11551b2","timestamp":"2026-07-16T09:55:48.403Z","message":{"role":"toolResult","toolCallId":"call_PAu9xla4yN4gYcYkl3QRXuRe|fc_0cc7a0c9a797f232016a58aaa44be8819a87a2affbb44f991d","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n## Cargo.toml\n 55|# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n*56|# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n 57|[target.'cfg(unix)'.dependencies]\n 58|libc = \"0.2\"\n 59|\n...\n 61|tempfile = \"3\"\n*62|# Integration tests are separate crates and do not inherit the lib's deps, so the\n 63|# digest int-test re-declares the two it touches directly: the manifest registry\n 64|# (the mock adapter's `[history]` seam) and the perch-path/info resolution the\n 65|# on-demand projection reads.\n\n## src/\n### applyhost.rs\n 296|/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n*297|/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n 298|/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299|/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n### brainproc.rs\n 155|///\n*156|/// It inherits the broker's already-unelevated token as a plain child, so it\n 157|/// runs no de-elevation guard of its own.\n 158|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 159|pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n...\n 219|    // this is forward-correct: the gate is defined on the real readiness\n*220|    // semantic the live-agent adapter inherits.\n 221|    write_ready(generation);\n 222|\n 223|    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n...\n 368|    // tolerates its absence). Never fail the ready write over a diagnostic field.\n*369|    if let Some(hash) = current_exe_hash() {\n 370|        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n 371|    }\n 372|    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n...\n 375|/// Lowercase-hex SHA-256 of the running process's own executable file\n*376|/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n 377|/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n 378|/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n 379|/// read + hash per brain start — negligible, and it pays rent far beyond the\n 380|/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\n*381|fn current_exe_hash() -> Option<String> {\n*382|    let exe = std::env::current_exe().ok()?;\n 383|    let bytes = std::fs::read(exe).ok()?;\n 384|    Some(crate::release::sha256_hex(&bytes))\n 385|}\n...\n 744|/// Generic over `spawn_child` (which receives the **selected binary** — `None` =\n*745|/// the default `current_exe`, `Some(path)` = the rollback binary) and `env`, so\n 746|/// the unit harness injects fakes; production binds [`spawn_brain_child`] +\n 747|/// [`ProductionTrialEnv`].\n 748|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n...\n 996|/// Spawn the production brain child: re-exec `daemon brain` as a plain\n*997|/// (non-detached) managed child, so it inherits the broker's environment —\n*998|/// including `SPT_HOME` — and dies with no orphaning. NOT `spawn_detached`\n 999|/// (that path rebuilds the Windows env block and would drop `SPT_HOME`). The\n 1000|/// broker stamps `{generation, start-reason}` onto the argv (ADR-0018 V2).\n 1001|///\n...\n 1008|/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n*1009|/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n 1010|/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n 1011|/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n*1012|/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n 1013|/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n 1014|/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n 1015|/// record-driven `.old-N` selection); else the `canonical` path captured at\n 1016|/// broker start; else — only if that capture failed — degrade to a per-spawn\n*1017|/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n*1018|/// when a canonical path is available, because on Linux `current_exe()` follows\n 1019|/// the `apply` rename to the old binary.\n 1020|// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n 1021|fn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n...\n 1025|            Some(c) => Ok(c.to_path_buf()),\n*1026|            None => std::env::current_exe(),\n 1027|        },\n 1028|    }\n 1029|}\n...\n 1038|    let exe = select_brain_exe(binary, canonical)?;\n*1039|    let mut cmd = Command::new(exe);\n 1040|    cmd.args(brain_child_args(generation, reason));\n 1041|    // The brain is a background logic process — it must never flash a console\n 1042|    // window. On Windows a plain console-subsystem spawn pops one whenever the\n 1043|    // broker has a console (a `spt daemon run` in a terminal, or any test that\n*1044|    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n*1045|    // the plain-child SPT_HOME inheritance (above) is preserved.\n 1046|    #[cfg(windows)]\n 1047|    {\n 1048|        use std::os::windows::process::CommandExt;\n*1049|        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n*1050|        cmd.creation_flags(CREATE_NO_WINDOW);\n 1051|    }\n 1052|    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n 1053|    // OWN process group so the daemon can reap the brain + everything it spawns\n*1054|    // (the shellwake watchers, the detached Psyches — all inherit this group) as\n 1055|    // one unit on stop, without signalling the daemon itself. Windows roots the\n 1056|    // reap on a Job object instead (assigned in the supervisor via BrainReaper).\n 1057|    #[cfg(unix)]\n### broker.rs\n 1991|    // harnesshost) so the W3d live-update RESPAWN runs the real binary, not a\n*1992|    // verbatim path Command::new fails to find (which would spool idle inbound).\n 1993|    spt_runtime::runtime::resolve_translation_argv(\n 1994|        decl,\n 1995|        Some(install_dir),\n...\n 2269|    // AMBIENT env can NOT arm this: both vars are scrubbed at daemon startup\n*2270|    // (`spt_runtime::INJECT_ECHO_ENV_VARS`, F-036 env-inheritance class — an inherited\n 2271|    // dev-shell export once force-enabled echo-verify host-wide and a false miss RETYPED\n 2272|    // sequences into a live session's input). Only an explicit declaration (or an\n 2273|    // in-process test broker, which never runs the daemon entrypoints) reaches this read.\n...\n 3297|        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\n*3298|        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\n 3299|        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\n*3300|        // inherited env — empty for a legacy/N-1 spawn (inherit only).\n 3301|        let session = PtySession::spawn_program_in_env(\n 3302|            &req.program,\n 3303|            &req.args,\n### crc_swap.rs\n 512|            // leak; a plain kill+wait fully reaps it.\n*513|            std::process::Command::new(path)\n 514|                .args([\"-n\", \"30\", \"127.0.0.1\"])\n*515|                .stdout(std::process::Stdio::null())\n 516|                .spawn()\n 517|                .expect(\"spawn from the mapped image\")\n 518|        }\n...\n 520|        {\n*521|            std::process::Command::new(path)\n 522|                .arg(\"30\")\n 523|                .spawn()\n 524|                .expect(\"spawn from the mapped image\")\n### daemon.rs\n 47|        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n*48|        // a daemon restarted from inside an agent session inherits that\n 49|        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n 50|        // identity that is NEVER correct daemon state and would otherwise\n 51|        // leak into every spawn on the node. Scrub first, before any state\n...\n 89|        if crate::deelevate::is_elevated() {\n*90|            let exe = std::env::current_exe()?;\n 91|            match crate::deelevate::spawn_deelevated(\n 92|                &exe.to_string_lossy(),\n 93|                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n 94|                // unelevated daemon then runs detach_console + the null-handles guard,\n*95|                // so it never keeps live inherited stdio (matches every other rung).\n 96|                &[\"daemon\".to_string(), \"run\".to_string(), \"--detached\".to_string()],\n 97|            ) {\n 98|                Ok(Some(pid)) => {\n...\n 319|        // for symmetry, never raised (the daemon runs until killed). The brain\n*320|        // child inherits this process's already-unelevated token.\n 321|        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 322|        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n 323|        // container the supervisor enrolls every (re)spawned brain into. Created\n...\n 499|/// Service-aware (REQ-DAEMON-8): when an OS service manager has a registered\n*500|/// spt-daemon, this starts THAT — never a competing manual `spawn_detached`\n 501|/// daemon that would fight the service for the broker socket (the kitsubito\n 502|/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n 503|// [impl->REQ-DAEMON-8]\n...\n 516|                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n*517|                spawn_detached()?;\n 518|            }\n 519|        }\n*520|        crate::service::StartPlan::ManualSpawn => spawn_detached()?,\n 521|        // `running` was passed false, so AlreadyRunning is unreachable here.\n 522|        crate::service::StartPlan::AlreadyRunning => return Ok(()),\n 523|    }\n...\n 566|                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n*567|                spawn_detached()?;\n 568|                wait_until_up()?;\n 569|                Ok(StartOutcome::Spawned)\n 570|            }\n...\n 572|        crate::service::StartPlan::ManualSpawn => {\n*573|            spawn_detached()?;\n 574|            wait_until_up()?;\n 575|            Ok(StartOutcome::Spawned)\n 576|        }\n...\n 615|///\n*616|/// **Windows handle-inheritance guard (KNOWN-HAZARDS 5.6).** A caller may\n 617|/// capture this process's stdout/stderr through a pipe (`Command::output()`, a\n 618|/// harness hook reading the command's output). On Windows `CreateProcess` runs\n*619|/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n 620|/// pipe's write end and hold it open forever — the capturing reader never sees\n*621|/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n 622|/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n 623|/// (the first guard shipped here) still leaked a grandparent capture's pipe\n 624|/// sitting in our handle table as a stray. So detached long-lived children are\n*625|/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n 626|/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n 627|/// `CLOEXEC`) and keeps the plain `Command` path.\n 628|///\n...\n 632|/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n*633|/// no handle inheritance by construction, so 5.6 holds on that path too),\n 634|/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n 635|/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n 636|/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n...\n 638|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*639|fn spawn_detached() -> io::Result<()> {\n*640|    let exe = std::env::current_exe()?;\n 641|    #[cfg(windows)]\n 642|    {\n 643|        let exe = exe.to_string_lossy();\n...\n 651|        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n*652|        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n 653|        let args = [\n 654|            \"daemon\".to_string(),\n 655|            \"run\".to_string(),\n...\n 665|                    eprintln!(\n*666|                        \"ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \\\n 667|                         the elevated token\"\n 668|                    );\n 669|                }\n...\n 689|        use std::os::unix::process::CommandExt;\n*690|        let mut cmd = Command::new(exe);\n 691|        cmd.args([\"daemon\", \"run\"])\n*692|            .stdin(Stdio::null())\n### deelevate.rs\n 15|//!\n*16|//! - [`crate::daemon::spawn_detached`] de-elevates the child it launches —\n 17|//!   Windows: the UAC **linked token** ([`spawn_deelevated`]); Linux: drop\n 18|//!   the child to `SUDO_UID`/`SUDO_GID` with the invoker's `HOME`\n 19|//!   ([`sudo_invoker`] + `CommandExt::uid/gid`).\n...\n 32|//! they are re-declared here because the dependency arrow points the other\n*33|//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\n 34|//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\n 35|//! the MSVC default link set) — no new dependency for one seam.\n 36|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n...\n 417|\n*418|    // Raw advapi32/kernel32 declarations (the detached_no_inherit posture:\n 419|    // both libraries are in the MSVC default link set, no new dependency).\n 420|    #[repr(C)]\n 421|    struct StartupInfoW {\n...\n 449|        fn GetCurrentProcess() -> isize;\n*450|        fn OpenProcess(access: u32, inherit: i32, pid: u32) -> isize;\n 451|        fn OpenProcessToken(process: isize, access: u32, token: *mut isize) -> i32;\n 452|        fn GetTokenInformation(\n 453|            token: isize,\n...\n 486|    // The target user's environment block — CreateProcessWithTokenW with a\n*487|    // NULL environment inherits the CALLER's (elevated, possibly another\n 488|    // admin account's) variables, which would point the de-elevated daemon\n 489|    // at the wrong %LOCALAPPDATA% universe.\n 490|    #[link(name = \"userenv\")]\n...\n 494|            token: isize,\n*495|            inherit: i32,\n 496|        ) -> i32;\n 497|        fn DestroyEnvironmentBlock(env: *mut core::ffi::c_void) -> i32;\n 498|    }\n...\n 510|    // CreateProcessWithTokenW accepts only a RESTRICTED creation-flag set —\n*511|    // DETACHED_PROCESS and CREATE_NO_WINDOW (the detached_no_inherit combo)\n 512|    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second\n 513|    // elevated verification round). Detach therefore rides\n 514|    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup\n...\n 521|    /// tab-close does not reap the de-elevated daemon (see the unelevated path in\n*522|    /// `daemon::detached_no_inherit`). CreateProcessWithTokenW accepts only a\n 523|    /// RESTRICTED creation-flag set, so this flag may be rejected with\n 524|    /// ERROR_INVALID_PARAMETER (87) — `create_with_token` retries WITHOUT it in\n 525|    /// that case (the elevated daemon then relies on the no-job common case /\n...\n 577|    /// KH 5.6 holds by construction: `CreateProcessWithTokenW` never\n*578|    /// inherits handles (the API has no inherit flag and runs the child in\n 579|    /// a fresh handle table), so no captured caller's pipe can wedge on the\n 580|    /// immortal child.\n 581|    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n...\n 687|    /// `CreateProcessWithTokenW` under the same flags as\n*688|    /// `detached_no_inherit`, command line quoted by the shared\n 689|    /// [`quote_windows_arg`] rules. The environment is BUILT FOR THE TOKEN\n*690|    /// (`CreateEnvironmentBlock`) — inheriting the elevated caller's\n 691|    /// variables would point the child at the wrong `%LOCALAPPDATA%`\n 692|    /// universe when UAC elevated through a different admin account. A\n*693|    /// failed block build falls back to inheriting ours (same-account\n 694|    /// elevation resolves identically there).\n 695|    /// The invoker's explicit universe overrides that must survive the\n 696|    /// de-elevation respawn. `SPT_HOME` is the relocation knob\n...\n 748|        // chose — see `apply_env_overrides`. When the block build failed we pass\n*749|        // null (inherit ours), which already carries SPT_HOME.\n 750|        let overrides = deelevation_env_overrides();\n 751|        let mut overlaid: Vec<u16> = Vec::new();\n 752|        let env_ptr = if env_built && !overrides.is_empty() {\n### digest.rs\n 486|/// two-origin merge, REQ-TERM-7). The backbone keeps its structural order\n*487|/// (activity + spliced boundaries); a backbone item without its own `ts` inherits\n 488|/// the previous item's (carry-forward), so activity that predates timestamps still\n 489|/// orders sanely. A context entry sorts to its `ts`; on a tie it follows the\n 490|/// equal-ts backbone item (the injection shows just after the activity it\n...\n 623|    // [unit->REQ-TERM-7] merge_by_ts interleaves spt context entries into the\n*624|    // activity backbone by ts; a backbone item without ts inherits the previous\n 625|    // ts (carry-forward); a tie places the context entry after the activity.\n 626|    #[test]\n 627|    fn merge_by_ts_interleaves_context() {\n### endpoint.rs\n 7|//! processes never collide on one machine while a real install gets one stable\n*8|//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned\n 9|//! daemon computes the **same** names as the client that started it — that\n 10|//! shared derivation is the whole of the rendezvous (no registry lookup).\n 11|// [impl->REQ-DAEMON-3]\n### harnesshost.rs\n 221|/// `cwd`: the project-scoped working directory the harness PTY lands in\n*222|/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);\n 223|/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)\n 224|/// passes so the harness reads the RIGHT `.claude`/session history/digest source.\n 225|///\n...\n 400|    // becomes the absolute <install_dir>/<name>(.exe) argv[0] when that file\n*401|    // exists, so the broker's Command::new spawns the REAL binary instead of\n 402|    // failing against PATH (idle inbound spools). Bare fallback when the file is\n 403|    // absent. Mirrors runtime::resolve_program_in_dir's own tests.\n 404|    #[test]\n### lifecycle.rs\n 2385|            let manifest = live_manifest(drops.path());\n*2386|            // Node knob ON with a tiny threshold (the endpoint inherits it).\n 2387|            let cfg = DaemonConfig {\n 2388|                auto_suspend_after_ms: Some(1),\n 2389|                ..DaemonConfig::default()\n### livehost.rs\n 1801|        let spawn = |marker: &str| {\n*1802|            std::process::Command::new(\"cmd\")\n 1803|                .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n 1804|                .spawn()\n 1805|                .expect(\"spawn\")\n...\n 1811|        let spawn = |marker: &str| {\n*1812|            std::process::Command::new(\"sh\")\n 1813|                .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n 1814|                .spawn()\n 1815|                .expect(\"spawn\")\n### msg.rs\n 260|    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\n*261|    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\n 262|    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\n 263|    /// consumer (Claude Code) reads its `.claude`, session history, and digest\n 264|    /// source relative to cwd, so a project-scoped launcher MUST place the\n 265|    /// harness in the user's project root, not the daemon's. Additive +\n*266|    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\n 267|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 268|    pub cwd: Option<String>,\n 269|    /// Environment variables to INJECT into the spawned child, on top of the\n*270|    /// inherited env (manifest `[env.<VAR>]` direction = inject, with `{key}`\n*271|    /// substitution already applied — REQ-HAZARD-ENV-SUBST). Empty = inherit only\n 272|    /// (the legacy behavior). Additive + serde-defaulted: an N-1 brain/broker\n*273|    /// omits it and the child simply inherits the broker's env (no inject).\n 274|    #[serde(default, skip_serializing_if = \"std::collections::BTreeMap::is_empty\")]\n 275|    pub env: std::collections::BTreeMap<String, String>,\n 276|    /// The adapter's idle-delivery translation binary as a resolved **argv**\n...\n 1186|    // [unit->REQ-DAEMON-2] a net-dial request decodes with an unknown future field\n*1187|    // (the D4a net vocabulary inherits the catalog's forward-compat), and the\n 1188|    // dialed ack round-trips through its envelope helper.\n 1189|    #[test]\n 1190|    fn net_frames_round_trip_and_tolerate_unknown_fields() {\n### propagate.rs\n 338|///\n*339|/// The gate runs **twice by design** (REQ-UPD-2: per-node, never inherited):\n 340|/// [`verify_metadata`] on the offer before any bytes move, then the whole\n 341|/// [`plan_verified`] front door (metadata + artifact digest + classify) on\n 342|/// the real bytes before staging.\n### psyrelay.rs\n 160|/// The per-turn outbound driver: one bounded, stdout-captured Psyche turn\n*161|/// (the D7.5a driver — never `Stdio::null()`), parsed and relayed through the\n 162|/// sanitize boundary. A failed turn — including `EmptyOutput`, the\n 163|/// null-stdout shape 7.3 guards against — relays **nothing** and surfaces the\n 164|/// error. The production trigger loop (when a turn fires) rides D9; this is\n### reap.rs\n 12|//! daemon (it reaps, then returns from `Daemon::run` cleanly):\n*13|//!   * Windows: a Job object the brain is assigned to (its descendants inherit\n 14|//!     the job at creation — no per-grandchild bookkeeping, no assignment race\n 15|//!     for Psyches the brain spawns much later). `KILL_ON_JOB_CLOSE` is the\n 16|//!     crash backstop (a daemon that dies without a graceful stop still drops the\n...\n 19|//!   * Unix: a process group the brain leads (it `setpgid(0, 0)`s itself at spawn\n*20|//!     via `pre_exec`); its descendants inherit the group. The graceful path\n 21|//!     `kill(-pgid, SIGKILL)`s the group — the daemon is in its OWN group, so it\n 22|//!     is never signalled.\n 23|//!\n...\n 70|    /// one unit. Windows: assign the child to the Job (its later-spawned Psyches\n*71|    /// inherit it). Unix: record the child's pid as the group id (the child set\n 72|    /// its own group via `pre_exec` at spawn — see `spawn_brain_child`).\n 73|    pub fn enroll(&self, child: &Child) {\n 74|        #[cfg(windows)]\n...\n 196|\n*197|    /// Assign `child` to `job`. Its descendants inherit the job at creation.\n 198|    pub fn assign_to_job(job: isize, child: &Child) {\n 199|        let handle = child.as_raw_handle() as isize;\n 200|        let ok = unsafe { AssignProcessToJobObject(job, handle) };\n...\n 273|    // daemon→brain→Psyche shape, where the Psyche is a grandchild of the daemon\n*274|    // that inherits the brain's job/group. reap() must kill BOTH the child and the\n*275|    // grandchild, proving the OS-inheritance link by exercise, not by reasoning.\n 276|    #[cfg(windows)]\n 277|    #[test]\n 278|    fn job_reaps_enrolled_child_and_grandchild() {\n...\n 282|        // The child sleeps 1s BEFORE spawning the grandchild — the window the test\n*283|        // uses to enroll it into the job first, so the grandchild inherits the job.\n 284|        // Mirrors the real brain, which connects + signals ready long before it\n 285|        // ever hosts a Psyche. UseShellExecute=$false is LOAD-BEARING: Windows\n 286|        // PowerShell's default ShellExecute path spawns the grandchild OUTSIDE the\n 287|        // job (it breaks away and survives the reap); UseShellExecute=$false forces\n*288|        // CreateProcess, so the grandchild is a real child that inherits the job —\n 289|        // exactly the brain→Psyche spawn shape (null stdio + CreateProcess, no\n 290|        // breakaway). .Id → the pid file.\n 291|        let script = format!(\n...\n 300|        );\n*301|        let mut child = Command::new(\"powershell\")\n 302|            .args([\"-NoProfile\", \"-NonInteractive\", \"-Command\", &script])\n 303|            .spawn()\n 304|            .expect(\"spawn child\");\n...\n 310|        assert!(wait_exit(&mut child), \"reap must terminate the enrolled child\");\n*311|        assert!(wait_dead(gc), \"reap must terminate the inherited grandchild\");\n 312|        let _ = std::fs::remove_file(&pidfile);\n 313|    }\n 314|\n...\n 322|        // The child leads its OWN group (setpgid in pre_exec, applied before exec)\n*323|        // so the backgrounded grandchild (`sleep`) inherits the group regardless of\n 324|        // enroll timing; `$!` is the grandchild pid → the pid file; `wait` keeps\n 325|        // the child alive. The reap (kill -pgid) must take BOTH.\n*326|        let mut cmd = Command::new(\"sh\");\n 327|        cmd.arg(\"-c\").arg(format!(\n 328|            \"sleep 300 & echo $! > '{}'; wait\",\n 329|            pidfile.display()\n...\n 343|        assert!(wait_exit(&mut child), \"reap must terminate the enrolled child\");\n*344|        assert!(wait_dead(gc), \"reap must terminate the inherited grandchild\");\n 345|        let _ = std::fs::remove_file(&pidfile);\n 346|    }\n 347|\n### relcache.rs\n 669|\n*670|        // Restaging a bare single clears the stamp (never inherits the prior).\n 671|        cache.stage(&signed(8), b\"v8\").unwrap();\n 672|        assert_eq!(cache.staged_platform(), None, \"restage clears stale stamp\");\n 673|\n### service.rs\n 149|fn run_systemctl(args: &[&str]) -> io::Result<()> {\n*150|    let status = Command::new(\"systemctl\").args(args).status()?;\n 151|    if status.success() {\n 152|        Ok(())\n 153|    } else {\n...\n 169|    fn is_active(&self) -> bool {\n*170|        Command::new(\"systemctl\")\n 171|            .args([\"--user\", \"is-active\", \"--quiet\", \"spt-daemon.service\"])\n 172|            .status()\n 173|            .map(|s| s.success())\n...\n 185|    fn boot_hint(&self) -> Option<String> {\n*186|        let enabled = Command::new(\"systemctl\")\n 187|            .args([\"--user\", \"is-enabled\", \"--quiet\", \"spt-daemon.service\"])\n 188|            .status()\n 189|            .map(|s| s.success())\n...\n 202|fn logon_task_present() -> bool {\n*203|    Command::new(\"schtasks\")\n 204|        .args([\"/Query\", \"/TN\", \"spt-core daemon\"])\n*205|        .stdout(std::process::Stdio::null())\n*206|        .stderr(std::process::Stdio::null())\n 207|        .status()\n 208|        .map(|s| s.success())\n 209|        .unwrap_or(false)\n...\n 215|/// (confirmed live field-drift on ENLYZEAM). The safe forms are `daemon start`\n*216|/// (routes `spawn_detached` → `--detached`) or an explicit `daemon run --detached`.\n 217|/// Pure so both CI legs unit-test the classification. // [impl->REQ-HAZARD-DETACHED-DAEMON-STDIO]\n 218|pub fn logon_task_action_is_stale_bare(action: &str) -> bool {\n 219|    let a = action.to_lowercase();\n...\n 227|fn logon_task_action() -> Option<String> {\n*228|    let out = Command::new(\"schtasks\")\n 229|        .args([\"/Query\", \"/TN\", \"spt-core daemon\", \"/V\", \"/FO\", \"LIST\"])\n 230|        .output()\n 231|        .ok()?;\n...\n 335|    // the bare `daemon run` legacy form is stale (unsafe, no --detached); `daemon\n*336|    // start` (routes spawn_detached) and an explicit `daemon run --detached` are safe;\n 337|    // case-insensitive; an unrelated action is not flagged. RED-FIRST: dropping the\n 338|    // `!contains(\"--detached\")` guard flags the safe detached form.\n 339|    #[test]\n...\n 354|            !logon_task_action_is_stale_bare(r#\"\"C:\\spt.exe\" daemon start\"#),\n*355|            \"`daemon start` routes spawn_detached — safe\"\n 356|        );\n 357|        assert!(\n 358|            !logon_task_action_is_stale_bare(\"spt.exe endpoint list\"),\n### shellhost.rs\n 179|            // The shell binary is a LONG-LIVED detached child (KNOWN-HAZARDS\n*180|            // 5.6): on Windows it must inherit NO handles, or a caller\n 181|            // capturing `spt shell spawn`'s output — at any depth of the\n 182|            // process chain — hangs forever on a pipe write-end the immortal\n*183|            // child holds. Same no-inherit spawn as the daemon's.\n 184|            // [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]\n 185|            #[cfg(windows)]\n*186|            let pid = crate::daemon::detached_no_inherit(program, args)\n 187|                .map_err(|e| format!(\"spawn {program}: {e}\"))?;\n 188|            #[cfg(unix)]\n*189|            let pid = Command::new(program)\n 190|                .args(args)\n*191|                .stdin(Stdio::null())\n*192|                .stdout(Stdio::null())\n*193|                .stderr(Stdio::null())\n 194|                .spawn()\n 195|                .map_err(|e| format!(\"spawn {program}: {e}\"))?\n 196|                .id();\n...\n 232|            endpoint: shell_session_label(owner, shell_id),\n*233|            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is\n 234|            // a harness-bringup concern (REQ-HOST-RUN-2), not shell spawn.\n 235|            cwd: None,\n*236|            // Shells inherit the broker's env; [env] inject is a harness-bringup\n 237|            // concern (REQ-HAZARD-ENV-SUBST), not shell spawn.\n 238|            env: Default::default(),\n 239|            translation_binary: None,\n...\n 319|        use std::os::windows::process::CommandExt;\n*320|        let _ = Command::new(\"taskkill\")\n 321|            .args([\"/PID\", &pid.to_string(), \"/T\", \"/F\"])\n*322|            .stdout(Stdio::null())\n*323|            .stderr(Stdio::null())\n*324|            .creation_flags(0x0800_0000) // CREATE_NO_WINDOW\n 325|            .status();\n 326|    }\n 327|    #[cfg(unix)]\n*328|    let _ = Command::new(\"kill\")\n 329|        .args([\"-9\", &pid.to_string()])\n*330|        .stdout(Stdio::null())\n*331|        .stderr(Stdio::null())\n 332|        .status();\n 333|}\n 334|\n\n### pump/\n#### mod.rs\n 46|//!\n*47|//! ## Wire discipline (inherited from the test suites, now production)\n 48|//!\n 49|//! Every pump write is fire-and-forget (`op_id: None`) — a journaled\n 50|//! `net_stream_send` blocks on its ack and DISCARDS interleaved events\n\nShowing files 1-20 of 103+. Use skip=20 for the next page, or narrow paths/pattern."}],"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":120,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/psyrelay.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/reap.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/service.rs","crates/spt-daemon/src/shellhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":2},{"path":"crates/spt-daemon/src/applyhost.rs","count":1},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":4},{"path":"crates/spt-daemon/src/crc_swap.rs","count":3},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/deelevate.rs","count":13},{"path":"crates/spt-daemon/src/digest.rs","count":2},{"path":"crates/spt-daemon/src/endpoint.rs","count":1},{"path":"crates/spt-daemon/src/harnesshost.rs","count":2},{"path":"crates/spt-daemon/src/lifecycle.rs","count":1},{"path":"crates/spt-daemon/src/livehost.rs","count":2},{"path":"crates/spt-daemon/src/msg.rs","count":6},{"path":"crates/spt-daemon/src/propagate.rs","count":1},{"path":"crates/spt-daemon/src/psyrelay.rs","count":1},{"path":"crates/spt-daemon/src/pump/mod.rs","count":1},{"path":"crates/spt-daemon/src/reap.rs","count":13},{"path":"crates/spt-daemon/src/relcache.rs","count":1},{"path":"crates/spt-daemon/src/service.rs","count":10},{"path":"crates/spt-daemon/src/shellhost.rs","count":16}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n## Cargo.toml\n 55│# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n*56│# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n 57│[target.'cfg(unix)'.dependencies]\n 58│libc = \"0.2\"\n 59│\n   │...\n 61│tempfile = \"3\"\n*62│# Integration tests are separate crates and do not inherit the lib's deps, so the\n 63│# digest int-test re-declares the two it touches directly: the manifest registry\n 64│# (the mock adapter's `[history]` seam) and the perch-path/info resolution the\n 65│# on-demand projection reads.\n\n## src/\n### applyhost.rs\n 296│/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n*297│/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n 298│/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299│/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300│/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n### brainproc.rs\n  155│///\n *156│/// It inherits the broker's already-unelevated token as a plain child, so it\n  157│/// runs no de-elevation guard of its own.\n  158│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n  159│pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n     │...\n  219│    // this is forward-correct: the gate is defined on the real readiness\n *220│    // semantic the live-agent adapter inherits.\n  221│    write_ready(generation);\n  222│\n  223│    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n     │...\n  368│    // tolerates its absence). Never fail the ready write over a diagnostic field.\n *369│    if let Some(hash) = current_exe_hash() {\n  370│        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n  371│    }\n  372│    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n     │...\n  375│/// Lowercase-hex SHA-256 of the running process's own executable file\n *376│/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n  377│/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n  378│/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n  379│/// read + hash per brain start — negligible, and it pays rent far beyond the\n  380│/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\n *381│fn current_exe_hash() -> Option<String> {\n *382│    let exe = std::env::current_exe().ok()?;\n  383│    let bytes = std::fs::read(exe).ok()?;\n  384│    Some(crate::release::sha256_hex(&bytes))\n  385│}\n     │...\n  744│/// Generic over `spawn_child` (which receives the **selected binary** — `None` =\n *745│/// the default `current_exe`, `Some(path)` = the rollback binary) and `env`, so\n  746│/// the unit harness injects fakes; production binds [`spawn_brain_child`] +\n  747│/// [`ProductionTrialEnv`].\n  748│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n     │...\n  996│/// Spawn the production brain child: re-exec `daemon brain` as a plain\n *997│/// (non-detached) managed child, so it inherits the broker's environment —\n *998│/// including `SPT_HOME` — and dies with no orphaning. NOT `spawn_detached`\n  999│/// (that path rebuilds the Windows env block and would drop `SPT_HOME`). The\n 1000│/// broker stamps `{generation, start-reason}` onto the argv (ADR-0018 V2).\n 1001│///\n     │...\n 1008│/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n*1009│/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n 1010│/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n 1011│/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n*1012│/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n 1013│/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n 1014│/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n 1015│/// record-driven `.old-N` selection); else the `canonical` path captured at\n 1016│/// broker start; else — only if that capture failed — degrade to a per-spawn\n*1017│/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n*1018│/// when a canonical path is available, because on Linux `current_exe()` follows\n 1019│/// the `apply` rename to the old binary.\n 1020│// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n 1021│fn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n     │...\n 1025│            Some(c) => Ok(c.to_path_buf()),\n*1026│            None => std::env::current_exe(),\n 1027│        },\n 1028│    }\n 1029│}\n     │...\n 1038│    let exe = select_brain_exe(binary, canonical)?;\n*1039│    let mut cmd = Command::new(exe);\n 1040│    cmd.args(brain_child_args(generation, reason));\n 1041│    // The brain is a background logic process — it must never flash a console\n 1042│    // window. On Windows a plain console-subsystem spawn pops one whenever the\n 1043│    // broker has a console (a `spt daemon run` in a terminal, or any test that\n*1044│    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n*1045│    // the plain-child SPT_HOME inheritance (above) is preserved.\n 1046│    #[cfg(windows)]\n 1047│    {\n 1048│        use std::os::windows::process::CommandExt;\n*1049│        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n*1050│        cmd.creation_flags(CREATE_NO_WINDOW);\n 1051│    }\n 1052│    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n 1053│    // OWN process group so the daemon can reap the brain + everything it spawns\n*1054│    // (the shellwake watchers, the detached Psyches — all inherit this group) as\n 1055│    // one unit on stop, without signalling the daemon itself. Windows roots the\n 1056│    // reap on a Job object instead (assigned in the supervisor via BrainReaper).\n 1057│    #[cfg(unix)]\n### broker.rs\n 1991│    // harnesshost) so the W3d live-update RESPAWN runs the real binary, not a\n*1992│    // verbatim path Command::new fails to find (which would spool idle inbound).\n 1993│    spt_runtime::runtime::resolve_translation_argv(\n 1994│        decl,\n 1995│        Some(install_dir),\n     │...\n 2269│    // AMBIENT env can NOT arm this: both vars are scrubbed at daemon startup\n*2270│    // (`spt_runtime::INJECT_ECHO_ENV_VARS`, F-036 env-inheritance class — an inherited\n 2271│    // dev-shell export once force-enabled echo-verify host-wide and a false miss RETYPED\n 2272│    // sequences into a live session's input). Only an explicit declaration (or an\n 2273│    // in-process test broker, which never runs the daemon entrypoints) reaches this read.\n     │...\n 3297│        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\n*3298│        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\n 3299│        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\n*3300│        // inherited env — empty for a legacy/N-1 spawn (inherit only).\n 3301│        let session = PtySession::spawn_program_in_env(\n 3302│            &req.program,\n 3303│            &req.args,\n### crc_swap.rs\n 512│            // leak; a plain kill+wait fully reaps it.\n*513│            std::process::Command::new(path)\n 514│                .args([\"-n\", \"30\", \"127.0.0.1\"])\n*515│                .stdout(std::process::Stdio::null())\n 516│                .spawn()\n 517│                .expect(\"spawn from the mapped image\")\n 518│        }\n    │...\n 520│        {\n*521│            std::process::Command::new(path)\n 522│                .arg(\"30\")\n 523│                .spawn()\n 524│                .expect(\"spawn from the mapped image\")\n### daemon.rs\n  47│        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n *48│        // a daemon restarted from inside an agent session inherits that\n  49│        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n  50│        // identity that is NEVER correct daemon state and would otherwise\n  51│        // leak into every spawn on the node. Scrub first, before any state\n    │...\n  89│        if crate::deelevate::is_elevated() {\n *90│            let exe = std::env::current_exe()?;\n  91│            match crate::deelevate::spawn_deelevated(\n  92│                &exe.to_string_lossy(),\n  93│                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n  94│                // unelevated daemon then runs detach_console + the null-handles guard,\n *95│                // so it never keeps live inherited stdio (matches every other rung).\n  96│                &[\"daemon\".to_string(), \"run\".to_string(), \"--detached\".to_string()],\n  97│            ) {\n  98│                Ok(Some(pid)) => {\n    │...\n 319│        // for symmetry, never raised (the daemon runs until killed). The brain\n*320│        // child inherits this process's already-unelevated token.\n 321│        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 322│        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n 323│        // container the supervisor enrolls every (re)spawned brain into. Created\n    │...\n 499│/// Service-aware (REQ-DAEMON-8): when an OS service manager has a registered\n*500│/// spt-daemon, this starts THAT — never a competing manual `spawn_detached`\n 501│/// daemon that would fight the service for the broker socket (the kitsubito\n 502│/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n 503│// [impl->REQ-DAEMON-8]\n    │...\n 516│                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n*517│                spawn_detached()?;\n 518│            }\n 519│        }\n*520│        crate::service::StartPlan::ManualSpawn => spawn_detached()?,\n 521│        // `running` was passed false, so AlreadyRunning is unreachable here.\n 522│        crate::service::StartPlan::AlreadyRunning => return Ok(()),\n 523│    }\n    │...\n 566│                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n*567│                spawn_detached()?;\n 568│                wait_until_up()?;\n 569│                Ok(StartOutcome::Spawned)\n 570│            }\n    │...\n 572│        crate::service::StartPlan::ManualSpawn => {\n*573│            spawn_detached()?;\n 574│            wait_until_up()?;\n 575│            Ok(StartOutcome::Spawned)\n 576│        }\n    │...\n 615│///\n*616│/// **Windows handle-inheritance guard (KNOWN-HAZARDS 5.6).** A caller may\n 617│/// capture this process's stdout/stderr through a pipe (`Command::output()`, a\n 618│/// harness hook reading the command's output). On Windows `CreateProcess` runs\n*619│/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n 620│/// pipe's write end and hold it open forever — the capturing reader never sees\n*621│/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n 622│/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n 623│/// (the first guard shipped here) still leaked a grandparent capture's pipe\n 624│/// sitting in our handle table as a stray. So detached long-lived children are\n*625│/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n 626│/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n 627│/// `CLOEXEC`) and keeps the plain `Command` path.\n 628│///\n    │...\n 632│/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n*633│/// no handle inheritance by construction, so 5.6 holds on that path too),\n 634│/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n 635│/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n 636│/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n    │...\n 638│// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*639│fn spawn_detached() -> io::Result<()> {\n*640│    let exe = std::env::current_exe()?;\n 641│    #[cfg(windows)]\n 642│    {\n 643│        let exe = exe.to_string_lossy();\n    │...\n 651│        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n*652│        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n 653│        let args = [\n 654│            \"daemon\".to_string(),\n 655│            \"run\".to_string(),\n    │...\n 665│                    eprintln!(\n*666│                        \"ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \\\n 667│                         the elevated token\"\n 668│                    );\n 669│                }\n    │...\n 689│        use std::os::unix::process::CommandExt;\n*690│        let mut cmd = Command::new(exe);\n 691│        cmd.args([\"daemon\", \"run\"])\n*692│            .stdin(Stdio::null())\n### deelevate.rs\n  15│//!\n *16│//! - [`crate::daemon::spawn_detached`] de-elevates the child it launches —\n  17│//!   Windows: the UAC **linked token** ([`spawn_deelevated`]); Linux: drop\n  18│//!   the child to `SUDO_UID`/`SUDO_GID` with the invoker's `HOME`\n  19│//!   ([`sudo_invoker`] + `CommandExt::uid/gid`).\n    │...\n  32│//! they are re-declared here because the dependency arrow points the other\n *33│//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\n  34│//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\n  35│//! the MSVC default link set) — no new dependency for one seam.\n  36│// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n    │...\n 417│\n*418│    // Raw advapi32/kernel32 declarations (the detached_no_inherit posture:\n 419│    // both libraries are in the MSVC default link set, no new dependency).\n 420│    #[repr(C)]\n 421│    struct StartupInfoW {\n    │...\n 449│        fn GetCurrentProcess() -> isize;\n*450│        fn OpenProcess(access: u32, inherit: i32, pid: u32) -> isize;\n 451│        fn OpenProcessToken(process: isize, access: u32, token: *mut isize) -> i32;\n 452│        fn GetTokenInformation(\n 453│            token: isize,\n    │...\n 486│    // The target user's environment block — CreateProcessWithTokenW with a\n*487│    // NULL environment inherits the CALLER's (elevated, possibly another\n 488│    // admin account's) variables, which would point the de-elevated daemon\n 489│    // at the wrong %LOCALAPPDATA% universe.\n 490│    #[link(name = \"userenv\")]\n    │...\n 494│            token: isize,\n*495│            inherit: i32,\n 496│        ) -> i32;\n 497│        fn DestroyEnvironmentBlock(env: *mut core::ffi::c_void) -> i32;\n 498│    }\n    │...\n 510│    // CreateProcessWithTokenW accepts only a RESTRICTED creation-flag set —\n*511│    // DETACHED_PROCESS and CREATE_NO_WINDOW (the detached_no_inherit combo)\n 512│    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second\n 513│    // elevated verification round). Detach therefore rides\n 514│    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup\n    │...\n 521│    /// tab-close does not reap the de-elevated daemon (see the unelevated path in\n*522│    /// `daemon::detached_no_inherit`). CreateProcessWithTokenW accepts only a\n 523│    /// RESTRICTED creation-flag set, so this flag may be rejected with\n 524│    /// ERROR_INVALID_PARAMETER (87) — `create_with_token` retries WITHOUT it in\n 525│    /// that case (the elevated daemon then relies on the no-job common case /\n    │...\n 577│    /// KH 5.6 holds by construction: `CreateProcessWithTokenW` never\n*578│    /// inherits handles (the API has no inherit flag and runs the child in\n 579│    /// a fresh handle table), so no captured caller's pipe can wedge on the\n 580│    /// immortal child.\n 581│    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n    │...\n 687│    /// `CreateProcessWithTokenW` under the same flags as\n*688│    /// `detached_no_inherit`, command line quoted by the shared\n 689│    /// [`quote_windows_arg`] rules. The environment is BUILT FOR THE TOKEN\n*690│    /// (`CreateEnvironmentBlock`) — inheriting the elevated caller's\n 691│    /// variables would point the child at the wrong `%LOCALAPPDATA%`\n 692│    /// universe when UAC elevated through a different admin account. A\n*693│    /// failed block build falls back to inheriting ours (same-account\n 694│    /// elevation resolves identically there).\n 695│    /// The invoker's explicit universe overrides that must survive the\n 696│    /// de-elevation respawn. `SPT_HOME` is the relocation knob\n    │...\n 748│        // chose — see `apply_env_overrides`. When the block build failed we pass\n*749│        // null (inherit ours), which already carries SPT_HOME.\n 750│        let overrides = deelevation_env_overrides();\n 751│        let mut overlaid: Vec<u16> = Vec::new();\n 752│        let env_ptr = if env_built && !overrides.is_empty() {\n### digest.rs\n 486│/// two-origin merge, REQ-TERM-7). The backbone keeps its structural order\n*487│/// (activity + spliced boundaries); a backbone item without its own `ts` inherits\n 488│/// the previous item's (carry-forward), so activity that predates timestamps still\n 489│/// orders sanely. A context entry sorts to its `ts`; on a tie it follows the\n 490│/// equal-ts backbone item (the injection shows just after the activity it\n    │...\n 623│    // [unit->REQ-TERM-7] merge_by_ts interleaves spt context entries into the\n*624│    // activity backbone by ts; a backbone item without ts inherits the previous\n 625│    // ts (carry-forward); a tie places the context entry after the activity.\n 626│    #[test]\n 627│    fn merge_by_ts_interleaves_context() {\n### endpoint.rs\n  7│//! processes never collide on one machine while a real install gets one stable\n *8│//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned\n  9│//! daemon computes the **same** names as the client that started it — that\n 10│//! shared derivation is the whole of the rendezvous (no registry lookup).\n 11│// [impl->REQ-DAEMON-3]\n### harnesshost.rs\n 221│/// `cwd`: the project-scoped working directory the harness PTY lands in\n*222│/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);\n 223│/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)\n 224│/// passes so the harness reads the RIGHT `.claude`/session history/digest source.\n 225│///\n    │...\n 400│    // becomes the absolute <install_dir>/<name>(.exe) argv[0] when that file\n*401│    // exists, so the broker's Command::new spawns the REAL binary instead of\n 402│    // failing against PATH (idle inbound spools). Bare fallback when the file is\n 403│    // absent. Mirrors runtime::resolve_program_in_dir's own tests.\n 404│    #[test]\n### lifecycle.rs\n 2385│            let manifest = live_manifest(drops.path());\n*2386│            // Node knob ON with a tiny threshold (the endpoint inherits it).\n 2387│            let cfg = DaemonConfig {\n 2388│                auto_suspend_after_ms: Some(1),\n 2389│                ..DaemonConfig::default()\n### livehost.rs\n 1801│        let spawn = |marker: &str| {\n*1802│            std::process::Command::new(\"cmd\")\n 1803│                .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n 1804│                .spawn()\n 1805│                .expect(\"spawn\")\n     │...\n 1811│        let spawn = |marker: &str| {\n*1812│            std::process::Command::new(\"sh\")\n 1813│                .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n 1814│                .spawn()\n 1815│                .expect(\"spawn\")\n### msg.rs\n  260│    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\n *261│    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\n  262│    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\n  263│    /// consumer (Claude Code) reads its `.claude`, session history, and digest\n  264│    /// source relative to cwd, so a project-scoped launcher MUST place the\n  265│    /// harness in the user's project root, not the daemon's. Additive +\n *266│    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\n  267│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n  268│    pub cwd: Option<String>,\n  269│    /// Environment variables to INJECT into the spawned child, on top of the\n *270│    /// inherited env (manifest `[env.<VAR>]` direction = inject, with `{key}`\n *271│    /// substitution already applied — REQ-HAZARD-ENV-SUBST). Empty = inherit only\n  272│    /// (the legacy behavior). Additive + serde-defaulted: an N-1 brain/broker\n *273│    /// omits it and the child simply inherits the broker's env (no inject).\n  274│    #[serde(default, skip_serializing_if = \"std::collections::BTreeMap::is_empty\")]\n  275│    pub env: std::collections::BTreeMap<String, String>,\n  276│    /// The adapter's idle-delivery translation binary as a resolved **argv**\n     │...\n 1186│    // [unit->REQ-DAEMON-2] a net-dial request decodes with an unknown future field\n*1187│    // (the D4a net vocabulary inherits the catalog's forward-compat), and the\n 1188│    // dialed ack round-trips through its envelope helper.\n 1189│    #[test]\n 1190│    fn net_frames_round_trip_and_tolerate_unknown_fields() {\n### propagate.rs\n 338│///\n*339│/// The gate runs **twice by design** (REQ-UPD-2: per-node, never inherited):\n 340│/// [`verify_metadata`] on the offer before any bytes move, then the whole\n 341│/// [`plan_verified`] front door (metadata + artifact digest + classify) on\n 342│/// the real bytes before staging.\n### psyrelay.rs\n 160│/// The per-turn outbound driver: one bounded, stdout-captured Psyche turn\n*161│/// (the D7.5a driver — never `Stdio::null()`), parsed and relayed through the\n 162│/// sanitize boundary. A failed turn — including `EmptyOutput`, the\n 163│/// null-stdout shape 7.3 guards against — relays **nothing** and surfaces the\n 164│/// error. The production trigger loop (when a turn fires) rides D9; this is\n### reap.rs\n  12│//! daemon (it reaps, then returns from `Daemon::run` cleanly):\n *13│//!   * Windows: a Job object the brain is assigned to (its descendants inherit\n  14│//!     the job at creation — no per-grandchild bookkeeping, no assignment race\n  15│//!     for Psyches the brain spawns much later). `KILL_ON_JOB_CLOSE` is the\n  16│//!     crash backstop (a daemon that dies without a graceful stop still drops the\n    │...\n  19│//!   * Unix: a process group the brain leads (it `setpgid(0, 0)`s itself at spawn\n *20│//!     via `pre_exec`); its descendants inherit the group. The graceful path\n  21│//!     `kill(-pgid, SIGKILL)`s the group — the daemon is in its OWN group, so it\n  22│//!     is never signalled.\n  23│//!\n    │...\n  70│    /// one unit. Windows: assign the child to the Job (its later-spawned Psyches\n *71│    /// inherit it). Unix: record the child's pid as the group id (the child set\n  72│    /// its own group via `pre_exec` at spawn — see `spawn_brain_child`).\n  73│    pub fn enroll(&self, child: &Child) {\n  74│        #[cfg(windows)]\n    │...\n 196│\n*197│    /// Assign `child` to `job`. Its descendants inherit the job at creation.\n 198│    pub fn assign_to_job(job: isize, child: &Child) {\n 199│        let handle = child.as_raw_handle() as isize;\n 200│        let ok = unsafe { AssignProcessToJobObject(job, handle) };\n    │...\n 273│    // daemon→brain→Psyche shape, where the Psyche is a grandchild of the daemon\n*274│    // that inherits the brain's job/group. reap() must kill BOTH the child and the\n*275│    // grandchild, proving the OS-inheritance link by exercise, not by reasoning.\n 276│    #[cfg(windows)]\n 277│    #[test]\n 278│    fn job_reaps_enrolled_child_and_grandchild() {\n    │...\n 282│        // The child sleeps 1s BEFORE spawning the grandchild — the window the test\n*283│        // uses to enroll it into the job first, so the grandchild inherits the job.\n 284│        // Mirrors the real brain, which connects + signals ready long before it\n 285│        // ever hosts a Psyche. UseShellExecute=$false is LOAD-BEARING: Windows\n 286│        // PowerShell's default ShellExecute path spawns the grandchild OUTSIDE the\n 287│        // job (it breaks away and survives the reap); UseShellExecute=$false forces\n*288│        // CreateProcess, so the grandchild is a real child that inherits the job —\n 289│        // exactly the brain→Psyche spawn shape (null stdio + CreateProcess, no\n 290│        // breakaway). .Id → the pid file.\n 291│        let script = format!(\n    │...\n 300│        );\n*301│        let mut child = Command::new(\"powershell\")\n 302│            .args([\"-NoProfile\", \"-NonInteractive\", \"-Command\", &script])\n 303│            .spawn()\n 304│            .expect(\"spawn child\");\n    │...\n 310│        assert!(wait_exit(&mut child), \"reap must terminate the enrolled child\");\n*311│        assert!(wait_dead(gc), \"reap must terminate the inherited grandchild\");\n 312│        let _ = std::fs::remove_file(&pidfile);\n 313│    }\n 314│\n    │...\n 322│        // The child leads its OWN group (setpgid in pre_exec, applied before exec)\n*323│        // so the backgrounded grandchild (`sleep`) inherits the group regardless of\n 324│        // enroll timing; `$!` is the grandchild pid → the pid file; `wait` keeps\n 325│        // the child alive. The reap (kill -pgid) must take BOTH.\n*326│        let mut cmd = Command::new(\"sh\");\n 327│        cmd.arg(\"-c\").arg(format!(\n 328│            \"sleep 300 & echo $! > '{}'; wait\",\n 329│            pidfile.display()\n    │...\n 343│        assert!(wait_exit(&mut child), \"reap must terminate the enrolled child\");\n*344│        assert!(wait_dead(gc), \"reap must terminate the inherited grandchild\");\n 345│        let _ = std::fs::remove_file(&pidfile);\n 346│    }\n 347│\n### relcache.rs\n 669│\n*670│        // Restaging a bare single clears the stamp (never inherits the prior).\n 671│        cache.stage(&signed(8), b\"v8\").unwrap();\n 672│        assert_eq!(cache.staged_platform(), None, \"restage clears stale stamp\");\n 673│\n### service.rs\n 149│fn run_systemctl(args: &[&str]) -> io::Result<()> {\n*150│    let status = Command::new(\"systemctl\").args(args).status()?;\n 151│    if status.success() {\n 152│        Ok(())\n 153│    } else {\n    │...\n 169│    fn is_active(&self) -> bool {\n*170│        Command::new(\"systemctl\")\n 171│            .args([\"--user\", \"is-active\", \"--quiet\", \"spt-daemon.service\"])\n 172│            .status()\n 173│            .map(|s| s.success())\n    │...\n 185│    fn boot_hint(&self) -> Option<String> {\n*186│        let enabled = Command::new(\"systemctl\")\n 187│            .args([\"--user\", \"is-enabled\", \"--quiet\", \"spt-daemon.service\"])\n 188│            .status()\n 189│            .map(|s| s.success())\n    │...\n 202│fn logon_task_present() -> bool {\n*203│    Command::new(\"schtasks\")\n 204│        .args([\"/Query\", \"/TN\", \"spt-core daemon\"])\n*205│        .stdout(std::process::Stdio::null())\n*206│        .stderr(std::process::Stdio::null())\n 207│        .status()\n 208│        .map(|s| s.success())\n 209│        .unwrap_or(false)\n    │...\n 215│/// (confirmed live field-drift on ENLYZEAM). The safe forms are `daemon start`\n*216│/// (routes `spawn_detached` → `--detached`) or an explicit `daemon run --detached`.\n 217│/// Pure so both CI legs unit-test the classification. // [impl->REQ-HAZARD-DETACHED-DAEMON-STDIO]\n 218│pub fn logon_task_action_is_stale_bare(action: &str) -> bool {\n 219│    let a = action.to_lowercase();\n    │...\n 227│fn logon_task_action() -> Option<String> {\n*228│    let out = Command::new(\"schtasks\")\n 229│        .args([\"/Query\", \"/TN\", \"spt-core daemon\", \"/V\", \"/FO\", \"LIST\"])\n 230│        .output()\n 231│        .ok()?;\n    │...\n 335│    // the bare `daemon run` legacy form is stale (unsafe, no --detached); `daemon\n*336│    // start` (routes spawn_detached) and an explicit `daemon run --detached` are safe;\n 337│    // case-insensitive; an unrelated action is not flagged. RED-FIRST: dropping the\n 338│    // `!contains(\"--detached\")` guard flags the safe detached form.\n 339│    #[test]\n    │...\n 354│            !logon_task_action_is_stale_bare(r#\"\"C:\\spt.exe\" daemon start\"#),\n*355│            \"`daemon start` routes spawn_detached — safe\"\n 356│        );\n 357│        assert!(\n 358│            !logon_task_action_is_stale_bare(\"spt.exe endpoint list\"),\n### shellhost.rs\n 179│            // The shell binary is a LONG-LIVED detached child (KNOWN-HAZARDS\n*180│            // 5.6): on Windows it must inherit NO handles, or a caller\n 181│            // capturing `spt shell spawn`'s output — at any depth of the\n 182│            // process chain — hangs forever on a pipe write-end the immortal\n*183│            // child holds. Same no-inherit spawn as the daemon's.\n 184│            // [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]\n 185│            #[cfg(windows)]\n*186│            let pid = crate::daemon::detached_no_inherit(program, args)\n 187│                .map_err(|e| format!(\"spawn {program}: {e}\"))?;\n 188│            #[cfg(unix)]\n*189│            let pid = Command::new(program)\n 190│                .args(args)\n*191│                .stdin(Stdio::null())\n*192│                .stdout(Stdio::null())\n*193│                .stderr(Stdio::null())\n 194│                .spawn()\n 195│                .map_err(|e| format!(\"spawn {program}: {e}\"))?\n 196│                .id();\n    │...\n 232│            endpoint: shell_session_label(owner, shell_id),\n*233│            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is\n 234│            // a harness-bringup concern (REQ-HOST-RUN-2), not shell spawn.\n 235│            cwd: None,\n*236│            // Shells inherit the broker's env; [env] inject is a harness-bringup\n 237│            // concern (REQ-HAZARD-ENV-SUBST), not shell spawn.\n 238│            env: Default::default(),\n 239│            translation_binary: None,\n    │...\n 319│        use std::os::windows::process::CommandExt;\n*320│        let _ = Command::new(\"taskkill\")\n 321│            .args([\"/PID\", &pid.to_string(), \"/T\", \"/F\"])\n*322│            .stdout(Stdio::null())\n*323│            .stderr(Stdio::null())\n*324│            .creation_flags(0x0800_0000) // CREATE_NO_WINDOW\n 325│            .status();\n 326│    }\n 327│    #[cfg(unix)]\n*328│    let _ = Command::new(\"kill\")\n 329│        .args([\"-9\", &pid.to_string()])\n*330│        .stdout(Stdio::null())\n*331│        .stderr(Stdio::null())\n 332│        .status();\n 333│}\n 334│\n\n### pump/\n#### mod.rs\n 46│//!\n*47│//! ## Wire discipline (inherited from the test suites, now production)\n 48│//!\n 49│//! Every pump write is fire-and-forget (`op_id: None`) — a journaled\n 50│//! `net_stream_send` blocks on its ack and DISCARDS interleaved events"},"isError":false,"timestamp":1784195748402}}
{"type":"message","id":"4706fa92","parentId":"453d20fb","timestamp":"2026-07-16T09:56:06.428Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing controller stream architecture**\n\n**Tracing CLI attach connection flow**"},{"type":"toolCall","id":"call_9jZ644qvMX1lVTBPGEtMBkBw|fc_0cc7a0c9a797f232016a58aab65e44819a977a22b1cb8bf773","name":"read","arguments":{"i":"Reading CLI command definitions","path":"crates/spt/src/cli.rs","selector":"160-290,1370-1470,3970-4090,4890-5065,5135-5210"}},{"type":"toolCall","id":"call_g4Niy1vL33Wq8Pgs417gIWV0|fc_0cc7a0c9a797f232016a58aab65e54819aa95414e70181b3bf","name":"read","arguments":{"i":"Reading refresh apply implementation","path":"crates/spt-daemon/src/applyhost.rs","selector":"1-115,248-405,810-860,870-945"}},{"type":"toolCall","id":"call_Cu8PKmqIAqtJOAAHc97veoEn|fc_0cc7a0c9a797f232016a58aab65e5c819ab2ccecf1caaf470e","name":"read","arguments":{"i":"Reading supervisor signal flow","path":"crates/spt-daemon/src/brainproc.rs","selector":"1-175,175-260,680-860,860-1068"}},{"type":"toolCall","id":"call_RSGZXlfWD9s4sNBBGdUJ40Kg|fc_0cc7a0c9a797f232016a58aab65e64819ab9b6a0c6495512c9","name":"read","arguments":{"i":"Reading daemon startup boundaries","path":"crates/spt-daemon/src/daemon.rs","selector":"1-125,170-395,480-715,715-890"}},{"type":"toolCall","id":"call_kWiJ5Qi6nT56uJ3o35VhPilH|fc_0cc7a0c9a797f232016a58aab65e68819abd255d7be94140f3","name":"grep","arguments":{"i":"Locating restart broker dispatch","pattern":"KIND_BRAIN_RESTART|set_brain_restart|request_brain_restart|BrainRestarted|brain-restarted","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_x7jEAPVirbNuk66fpoR5CtMX|fc_0cc7a0c9a797f232016a58aab65e6c819aad8ee44b53edd6d8","name":"grep","arguments":{"i":"Locating controller connection ownership","pattern":"Role::Controller|Role::Viewer|ControllerSink|controllers|viewers|serve_attach|KIND_ATTACH|attach","path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_tsJAVCaH0YdbDg3m9nUi4ZAJ|fc_0cc7a0c9a797f232016a58aab65e74819aa0e3b82f778d2803","name":"read","arguments":{"i":"Reading refresh end-to-end test","path":"crates/spt/tests/daemon_refresh_e2e.rs","selector":null}},{"type":"toolCall","id":"call_uUyo64qoBnHLLdKfDgNCWJhU|fc_0cc7a0c9a797f232016a58aab65e78819abb4bf0f2b32c9c06","name":"grep","arguments":{"i":"Finding lifecycle focused tests","pattern":"refresh|restart|survive|controller|viewer|brain.*restart|attach.*restart|update","path":"crates/spt-daemon/tests;crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":44647,"output":692,"cacheRead":61952,"cacheWrite":0,"totalTokens":107291,"reasoningTokens":192,"cost":{"input":0.22323500000000002,"output":0.02076,"cacheRead":0.030976,"cacheWrite":0,"total":0.274971}},"stopReason":"toolUse","timestamp":1784195748446,"responseId":"resp_0cc7a0c9a797f232016a58aaa4d0c4819aadb26d6bfce59024","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cc7a0c9a797f232016a58aaa73c04819a9ba6ba942df0c559","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKq28Gr98o_UcLHY-MTzKa1RFUSnS-Y0L02guy40gqXQKfQabuFmuqfwAHFXmsnOZ_I-mNhYxXZWgbN6SNrxjXGFOHzgG9CY0MMBgblWv9Coa7Si6ndXUb28Pf_TTJ6-Ama6DNpamxUEXG9Y9JeKOMM2hjZf0OBsiMXSY6gZXkAjTxhLm6X82XZzmzxbCXMKNQYP6Ng8wJwzlrmN7WskjAspe_qSjko9omPysiCCXlTe9xUwvuzyFU75-W-OKfZDTipTwzT8MWRnNvSsIQAjqWfb5WerqmutWa5imc1pQqTSDUWIgF3FeOP1GdRik-WvvavVlR5pbZp3Ctfo3qL5KsJTSWgEk9as843OdKXzDaCJm89QQTdn390UWLEVH8MBFIp9F7gpPsyuFYucKIhkDfxnxiab6skiY2a4hclnmEhiwq18KFiIMjTXbdOY03lKgGIJ9zJ4oSkd7Q7LcxsGekVu9-qx9sCELZ8Celz-IdcKecZNPuXjLOh5pPiTWOxXbPRAhj4QNpubZU-zAIs0X_i7da_vRAwECLR9ob2qMLBzhfc2MEzexR5MJrIji8sDcLjifMeroM0WrP77cyj6pAaBPXD4yrknm1PDA8orPwQutJg1m2NDDI5V1YlSlASU9yJE9r81DKhkxwlQCTZpc34hRcVrOjgXDMDqzbGEWI5ESrKN6B2FdOZqvd8i0KTuM9AY-OsK19KAlIsxxkOfS7RY1suI6Y5j6ZNOITTGJLcQX07ymjMA-aN_NPR8duJrLdLiQ2spv6LDwVCcPDSE6w_leX3PkPpDwpXkeRr6nPH3O6aHpC8I_lbEXtz5cIzB9fCydTnMcpNfWcHQifFRrMbyGGUFa5gnrBWqAHlMIFGtKI6nyIJtrbpB0sWuZ1Y3TQUR2ADng04pHgpL6Y4JuH-57CDv1X4wwURdwUUaSqJHptiJJq6ISRBIT41yekZejpVJ--T3J2QIJX4I0-OwQ0NEj9KGQZyFaeCfeg_cUsLJnkIOitQj7p9V3KmoTK1-tK_q_WxFVH4Ax6-gDKJIf9RRfMQjINDGx-e4nCHul_k-xYuCmiK_fmNpy3zsgXPrpuZLihKPhkbehtncGKZLeck5e5R_YJVwyau9K361h72MAvyRrb_3JxKGwUA3_Z4spy-l0okUC4ZkM8XeTeTVeTv5HX-RDU78eVaZkV3EUk2wgLhz40UI4_K5knckInR5VHP4lMfoKJ2dw9EXikGHCmUMUYqnCsZaRohqnbAvllql3cm9lFiTrgOQ7mXUGmdpZJ2b9n6QIzug-QDkF5aHSgjSxeYXrY8G8tF5Mv3TyvdWmqhDx7nM7bM-VvAcq7X48I7cbxlUR8gMndeoseCPYr4CrkpH7wBd_qXsiu41MC8gpZJVlStVMhfrvCyOjZu4jfIUAVneiMWZXy7QEHn5HHQkSOGrkpfI0t41v9hc-IINqHKvmocV5G2JiA0_wBlIEn-ulIi37z99DDjF2wPGSnYWVYdIigOmsnSdIuBCxE8yY-CJy7re--lhyLwZyCTzXtFSJMySBmH8kOA0Dw426hXo0q9fhBD8dXxABiqxUr4QithP9U0SRDd8os31h6SZQiyGU0gLV49pVB0njpzI5sEeeSeccZXfi2tjF5OJCXLEBteJ0XX_QEeNWa0dus6gCf6ZiBOND5KQ0H_B5uwbd9Q3mpWZ1VehzjiMEAdUIHCZlIl1viGX8HlEQ5X5D8teWUVjHRoJAHDSR3cKDPAJDUd13ShG7zMj05_gue9ymHhkzEJidHSJpIwRrrzlykIIMIeJR0aV-19cBG2ZhkxLajgg_hsOV7HkyiCuBLJnVtPM14JB7eBedB48XcQlERiy2HBi39V5sOcdRCYiCXEXG_TJYIb0Y8POZSz8Wo4N8vqeR99TmfGAhlIj0rg5Pd8Pk_pQIFbu5Kz6a5F_5fNkuTOjHQmXaCMLPZLL-n4pM8svEufEO1s0vzj5zZLUuC2de9quvIzFqqqNKSsr4HLtYs5yI9y1HarK624jTT8289WcmC8IDZNYz99McJMMUS3Okp487NWgvWVHNu_WiMMJRAasrH_le9Wi8oO9YHw2LS7GyNw8glJq4n6rFGldcnVbzAf3flBC0xFSwmtcaj-YkbSyLY12d9igkmQr5ZJrq5wHJEdtUvynSS31vgYLBtY3jTGHcJQdo6oyuJhKznjaDV1k3A-Q4tTLZ_mdCovQBLVLLXVeZHjEfSs3WrNdR9t_cqsLsJYE7TaOHR-zBOJSqXplbXrVlyu_6qh1Awuk2xGPqv_3buB5cd_6VOA3dqUU17vyO5I5hg-uLWe5dphOc6RF59x6JhlWSNC_ZKVBD8mw6y4=","summary":[{"type":"summary_text","text":"**Analyzing controller stream architecture**"},{"type":"summary_text","text":"**Tracing CLI attach connection flow**"}]},{"id":"fc_0cc7a0c9a797f232016a58aab65e44819a977a22b1cb8bf773","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading CLI command definitions\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"160-290,1370-1470,3970-4090,4890-5065,5135-5210\"}","call_id":"call_9jZ644qvMX1lVTBPGEtMBkBw","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aab65e54819aa95414e70181b3bf","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh apply implementation\",\"path\":\"crates/spt-daemon/src/applyhost.rs\",\"selector\":\"1-115,248-405,810-860,870-945\"}","call_id":"call_g4Niy1vL33Wq8Pgs417gIWV0","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aab65e5c819ab2ccecf1caaf470e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading supervisor signal flow\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"1-175,175-260,680-860,860-1068\"}","call_id":"call_Cu8PKmqIAqtJOAAHc97veoEn","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aab65e64819ab9b6a0c6495512c9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon startup boundaries\",\"path\":\"crates/spt-daemon/src/daemon.rs\",\"selector\":\"1-125,170-395,480-715,715-890\"}","call_id":"call_RSGZXlfWD9s4sNBBGdUJ40Kg","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aab65e68819abd255d7be94140f3","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating restart broker dispatch\",\"pattern\":\"KIND_BRAIN_RESTART|set_brain_restart|request_brain_restart|BrainRestarted|brain-restarted\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_kWiJ5Qi6nT56uJ3o35VhPilH","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aab65e6c819aad8ee44b53edd6d8","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating controller connection ownership\",\"pattern\":\"Role::Controller|Role::Viewer|ControllerSink|controllers|viewers|serve_attach|KIND_ATTACH|attach\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_x7jEAPVirbNuk66fpoR5CtMX","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aab65e74819aa0e3b82f778d2803","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh end-to-end test\",\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"selector\":null}","call_id":"call_tsJAVCaH0YdbDg3m9nUi4ZAJ","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aab65e78819abb4bf0f2b32c9c06","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding lifecycle focused tests\",\"pattern\":\"refresh|restart|survive|controller|viewer|brain.*restart|attach.*restart|update\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_uUyo64qoBnHLLdKfDgNCWJhU","name":"grep"}]},"duration":17980.216199999675,"ttft":2779.323900000192,"contextSnapshot":{"promptTokens":106599,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9jZ644qvMX1lVTBPGEtMBkBw|fc_0cc7a0c9a797f232016a58aab65e44819a977a22b1cb8bf773","toolName":"read","startedAt":"2026-07-16T09:56:06.428Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading CLI command definitions"},"id":"5e996e2c","parentId":"4706fa92","timestamp":"2026-07-16T09:56:06.428Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_g4Niy1vL33Wq8Pgs417gIWV0|fc_0cc7a0c9a797f232016a58aab65e54819aa95414e70181b3bf","toolName":"read","startedAt":"2026-07-16T09:56:06.428Z","args":{"path":"crates/spt-daemon/src/applyhost.rs"},"intent":"Reading refresh apply implementation"},"id":"6b9d8946","parentId":"5e996e2c","timestamp":"2026-07-16T09:56:06.428Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Cu8PKmqIAqtJOAAHc97veoEn|fc_0cc7a0c9a797f232016a58aab65e5c819ab2ccecf1caaf470e","toolName":"read","startedAt":"2026-07-16T09:56:06.428Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading supervisor signal flow"},"id":"e9aeda24","parentId":"6b9d8946","timestamp":"2026-07-16T09:56:06.428Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RSGZXlfWD9s4sNBBGdUJ40Kg|fc_0cc7a0c9a797f232016a58aab65e64819ab9b6a0c6495512c9","toolName":"read","startedAt":"2026-07-16T09:56:06.429Z","args":{"path":"crates/spt-daemon/src/daemon.rs"},"intent":"Reading daemon startup boundaries"},"id":"70718f36","parentId":"e9aeda24","timestamp":"2026-07-16T09:56:06.429Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kWiJ5Qi6nT56uJ3o35VhPilH|fc_0cc7a0c9a797f232016a58aab65e68819abd255d7be94140f3","toolName":"grep","startedAt":"2026-07-16T09:56:06.429Z","args":{"path":"crates/spt-daemon/src"},"intent":"Locating restart broker dispatch"},"id":"26f57db9","parentId":"70718f36","timestamp":"2026-07-16T09:56:06.429Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_x7jEAPVirbNuk66fpoR5CtMX|fc_0cc7a0c9a797f232016a58aab65e6c819aad8ee44b53edd6d8","toolName":"grep","startedAt":"2026-07-16T09:56:06.429Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs"},"intent":"Locating controller connection ownership"},"id":"98700d89","parentId":"26f57db9","timestamp":"2026-07-16T09:56:06.429Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_tsJAVCaH0YdbDg3m9nUi4ZAJ|fc_0cc7a0c9a797f232016a58aab65e74819aa0e3b82f778d2803","toolName":"read","startedAt":"2026-07-16T09:56:06.429Z","args":{"path":"crates/spt/tests/daemon_refresh_e2e.rs"},"intent":"Reading refresh end-to-end test"},"id":"6d676bff","parentId":"98700d89","timestamp":"2026-07-16T09:56:06.429Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_uUyo64qoBnHLLdKfDgNCWJhU|fc_0cc7a0c9a797f232016a58aab65e78819abb4bf0f2b32c9c06","toolName":"grep","startedAt":"2026-07-16T09:56:06.429Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests"},"intent":"Finding lifecycle focused tests"},"id":"34028c0c","parentId":"6d676bff","timestamp":"2026-07-16T09:56:06.429Z"}
{"type":"message","id":"d4ec0359","parentId":"34028c0c","timestamp":"2026-07-16T09:56:06.451Z","message":{"role":"toolResult","toolCallId":"call_Cu8PKmqIAqtJOAAHc97veoEn|fc_0cc7a0c9a797f232016a58aab65e5c819ab2ccecf1caaf470e","toolName":"read","content":[{"type":"text","text":"//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n//! broker/brain process-isolation restoration, D1 skeleton).\n//!\n//! ADR-0004 specified the broker/brain split as two **processes**; production\n//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n//! the binary on disk but never restarts the running code (the regression\n//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n//! `spt daemon brain` child. The child connects over the existing broker socket\n//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n//! verb. The digest hub serve + seed-control stay broker-side continuity\n//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n//! loops.\n//!\n//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n//! broker spawns + supervises a brain child over the socket, and the broker\n//! (with its PTY children + seed channel) survives the brain dying — the\n//! supervisor respawns it from the executable path, which on a real update is\n//! the swapped binary.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\nuse std::io;\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::endpoint::{brain_ready_path, broker_socket_name};\nuse crate::relcache::{AppliedRecord, ReleaseCache};\n\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n/// never a hot respawn loop.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n/// A run at least this long counts as healthy and resets the backoff floor.\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n/// brain has this long to signal `ready` for its generation before the window\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n/// **exits before** signaling ready is respawned this many consecutive times\n/// (a transient first-boot hiccup should not abandon a good binary) before the\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n/// window-expiry triggers rollback.\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// How long the brain child retries connecting to the broker socket before\n/// giving up (the broker may still be binding — the child is freshly spawned).\nconst BRAIN_CONNECT_TRIES: u32 = 300;\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\n/// reliable than a brain→brain frame, which retires in D4).\n///\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\n/// never a parse failure that would brick the brain before it rehydrates.\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum StartReason {\n    /// The broker's first brain spawn this broker lifetime.\n    Cold,\n    /// A supervised respawn after an unexpected brain exit.\n    Crash,\n    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n    Update,\n}\n\nimpl StartReason {\n    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n    pub fn as_arg(self) -> &'static str {\n        match self {\n            StartReason::Cold => \"cold\",\n            StartReason::Crash => \"crash\",\n            StartReason::Update => \"update\",\n        }\n    }\n\n    /// Parse the `--start-reason` argv value, lenient: an unknown token (a\n    /// newer broker's reason an older brain predates) defaults to `Cold` rather\n    /// than failing the parse (KH-2.3 — defaults survive intermediate versions).\n    pub fn from_arg(s: &str) -> Self {\n        match s {\n            \"update\" => StartReason::Update,\n            \"crash\" => StartReason::Crash,\n            _ => StartReason::Cold,\n        }\n    }\n}\n\n/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n/// its next wait tick, kills the current brain child, and respawns it with\n/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n/// between the broker serve path and the supervisor — both run in the broker\n/// process. This is the seam that makes a routine `apply` actually run new code,\n/// the pillar the in-process-collapse regression silently unrealized.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n#[derive(Default)]\npub struct BrainRestart {\n    requested: AtomicBool,\n}\n\nimpl BrainRestart {\n    /// A fresh signal with no pending request.\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Request a planned restart (broker handler side) — idempotent: coalesces\n    /// with any already-pending request into one cycle.\n    pub fn request(&self) {\n        self.requested.store(true, Ordering::SeqCst);\n    }\n\n    /// Consume a pending request (supervisor side): true at most once per\n    /// `request`, clearing it so the next cycle starts fresh.\n    pub fn take(&self) -> bool {\n        self.requested.swap(false, Ordering::SeqCst)\n    }\n}\n\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n/// holding the connection and probing broker liveness each heartbeat. A dropped\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n/// from the (possibly swapped) binary.\n///\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n…\n    }\n}\n…\nfn run_trial(\n    child: &mut Child,\n    generation: u64,\n    window: Duration,\n    env: &dyn TrialEnv,\n    stop: &AtomicBool,\n) -> TrialStep {\n    let deadline = Instant::now() + window;\n    let mut ready_seen = false;\n    loop {\n        if stop.load(Ordering::Relaxed) {\n            return TrialStep::Stopped;\n        }\n        // Latch readiness FIRST (don't demote a ready-then-exit child) — but do NOT\n        // promote on it alone: the old generation's control plane must have drained.\n        if !ready_seen && env.ready_generation() == Some(generation) {\n            ready_seen = true;\n        }\n        if ready_seen && env.old_gen_drained() {\n            return TrialStep::Promoted;\n        }\n        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n        // once ready-seen we are waiting on the OLD gen to drain, and a ready-then-\n        // exited child still promotes (the exit is a normal later respawn).\n        if !ready_seen {\n            match child.try_wait() {\n                Ok(Some(_)) => return TrialStep::PreReadyExit,\n                Ok(None) => {}\n                // A wait error: treat as a pre-ready exit (fail toward retry, never\n                // a panic in the supervisor thread).\n                Err(e) => {\n                    eprintln!(\"BRAIN_TRIAL_WAIT_FAIL: {e}\");\n                    return TrialStep::PreReadyExit;\n                }\n            }\n        }\n        if Instant::now() >= deadline {\n            return TrialStep::WindowElapsedAlive;\n        }\n        thread::sleep(TICK);\n    }\n}\n\n/// Supervise the brain child: spawn it, wait for it, and respawn on exit after a\n/// capped, doubling backoff — until `stop` is raised, which kills the live child\n/// and returns. The process-level sibling of [`crate::pump::supervise_pump`]\n/// (ADR-0018 Q2: the broker is the always-up anchor that respawns the brain;\n/// Q3: a planned update is just the brain self-exiting on a path the supervisor\n/// already recovers from).\n///\n/// **Readiness-gated promotion + auto-rollback (ADR-0018 Q7, D6-2).** When the\n/// injected `env` reports an unpromoted `AppliedPending` record, the spawn is a\n/// **trial** (the latch is the *record*, **A8** — regardless of start reason):\n/// the supervisor clears `brain.ready` (**A9**), spawns the candidate, and waits\n/// for it to stamp ready for ITS generation within `window`. It **promotes** on\n/// ready (writes `Applied`, the candidate is now the accepted binary and a later\n/// crash is a normal respawn); it **rolls back** to the last-known-good binary\n/// on budget exhaustion — [`TRIAL_PREREADY_BUDGET`] consecutive pre-ready exits\n/// **or** one alive-but-never-ready window-expiry (which **kills** the candidate\n/// first, **A11** — never two live brains). Binary selection is **record-driven**\n/// (a `RolledBack` record spawns the good `.old-N` binary, surviving a reboot for\n/// free), so no file is renamed at the failure instant. Every hosted endpoint is\n/// untouched throughout — the broker holds them across the whole cycle.\n///\n/// Generic over `spawn_child` (which receives the **selected binary** — `None` =\n/// the default `current_exe`, `Some(path)` = the rollback binary) and `env`, so\n/// the unit harness injects fakes; production binds [`spawn_brain_child`] +\n/// [`ProductionTrialEnv`].\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn supervise_brain(\n    stop: &AtomicBool,\n    restart: &BrainRestart,\n    base: Duration,\n    env: &dyn TrialEnv,\n    window: Duration,\n    mut spawn_child: impl FnMut(u64, StartReason, Option<&Path>) -> io::Result<Child>,\n) {\n    let mut backoff = base;\n    // Broker-owned generation custody (ADR-0018 V2, KH-2.4): the counter lives in\n    // the never-restarting supervisor and increments on EVERY spawn (cold or\n    // respawn), so the broker — which observes every respawn — is the\n    // authoritative source, not the outgoing brain. The first spawn is `Cold`;\n    // every supervised respawn here is a `Crash` (an unexpected exit the\n    // supervisor recovers from). A *planned* update respawn stamps `Update`\n    // instead — wired in D3-3, where the apply trigger drives this loop.\n    let mut generation: u64 = 0;\n    let mut reason = StartReason::Cold;\n    // Consecutive pre-ready exits of the CURRENT trial candidate (A8 budget K).\n    // In-memory: a broker restart resets it — acceptable, the trial restarts.\n    let mut pre_ready_exits: u32 = 0;\n    while !stop.load(Ordering::Relaxed) {\n        let started = Instant::now();\n        // Record-driven binary selection + trial latch (A8). The latch is the\n        // unpromoted `AppliedPending` record, NOT the start reason; a `RolledBack`\n        // record selects the last-known-good binary (and is never a trial — it is\n        // the recovery steady state). A missing/corrupt record degrades to \"no\n        // trial, default binary\" — the supervisor never panics on the record.\n        let record = env.applied_state();\n        let binary: Option<PathBuf> = match &record {\n            Some(AppliedRecord::RolledBack { rollback_binary, .. }) => {\n                Some(PathBuf::from(rollback_binary))\n            }\n            _ => None,\n        };\n        let is_trial = matches!(record, Some(AppliedRecord::AppliedPending { .. }));\n        // A9: clear `brain.ready` immediately before a trial spawn — the prior\n        // child is already waited-on dead (below), so the only stamp that can\n        // exist post-clear is THIS trial child's. Closes the cross-lifetime hole\n        // that same-generation equality alone cannot (a stale gen-N file from a\n        // pre-reboot lifetime matching a fresh gen-N trial).\n        if is_trial {\n            env.clear_ready();\n        }\n        // Did this cycle end because of a planned update restart (Q3) rather than\n        // an unexpected crash? The broker — observing the cycle it initiated — is\n        // the authoritative source of that distinction (V2).\n        let mut planned = false;\n        match spawn_child(generation, reason, binary.as_deref()) {\n            Ok(mut child) => {\n                // The trial gate runs first for a candidate on trial; on\n                // promotion it falls through to the normal wait loop for the SAME\n                // child. A non-trial spawn skips straight to normal supervision.\n                let mut supervise = !is_trial;\n                if is_trial {\n                    match run_trial(&mut child, generation, window, env, stop) {\n                        TrialStep::Stopped => {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        TrialStep::Promoted => {\n                            // Bytes-gate (KH 6.11): readiness != new-bytes. Promote\n                            // only if the candidate is running the staged bytes; a\n                            // ready-but-wrong-bytes candidate (e.g. a Linux respawn\n                            // onto the renamed old binary) fails the trial → kill +\n                            // rollback, never a falsely-`applied` record.\n                            let version = match &record {\n                                Some(AppliedRecord::AppliedPending { version, .. }) => Some(*version),\n                                _ => None,\n                            };\n                            if let Some(version) = version {\n                                if let BytesGate::Mismatch = bytes_gate(env, version) {\n                                    eprintln!(\"BRAIN_PROMOTE_REJECTED: candidate v{version} ready but running the WRONG bytes (generation {generation}) — rolling back (KH 6.11)\");\n                                    let _ = child.kill();\n                                    let _ = child.wait();\n                                    rollback(env, &record);\n                                    pre_ready_exits = 0;\n                                    generation += 1;\n                                    reason = StartReason::Crash;\n                                    backoff = base;\n                                    continue;\n                                }\n                                eprintln!(\"BRAIN_PROMOTED: candidate v{version} signalled ready (generation {generation}) — swap accepted\");\n                                env.record_promoted(version);\n                            }\n                            pre_ready_exits = 0;\n                            backoff = base;\n                            supervise = true; // keep supervising the now-accepted child\n                        }\n                        TrialStep::PreReadyExit => {\n                            pre_ready_exits += 1;\n                            eprintln!(\"BRAIN_TRIAL_EXIT: candidate exited pre-ready ({pre_ready_exits}/{TRIAL_PREREADY_BUDGET})\");\n                            if pre_ready_exits >= TRIAL_PREREADY_BUDGET {\n                                rollback(env, &record);\n                                pre_ready_exits = 0;\n                                // The rollback binary is known-good — respawn it\n                                // promptly, no crash-penalty backoff (mirror the\n                                // WindowElapsedAlive path); only the under-budget\n                                // candidate retries keep the doubling backoff.\n                                backoff = base;\n                            } else {\n                                backoff = next_backoff(backoff, started.elapsed(), base);\n                            }\n                            generation += 1;\n                            reason = StartReason::Crash;\n                            sleep_backoff(backoff, stop);\n                            continue;\n                        }\n                        TrialStep::WindowElapsedAlive => {\n                            // A11: KILL the alive-but-never-ready candidate BEFORE\n                            // spawning the rollback binary — two live brains would\n                            // break every single-brain seed/socket/IPC assumption.\n                            eprintln!(\"BRAIN_TRIAL_TIMEOUT: candidate alive but never ready — killing before rollback\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            rollback(env, &record);\n                            pre_ready_exits = 0;\n                            generation += 1;\n                            // The rollback binary is known-good — respawn promptly,\n                            // no crash-penalty backoff.\n                            reason = StartReason::Crash;\n                            backoff = base;\n                            continue;\n                        }\n                    }\n                }\n                if supervise {\n                    loop {\n                        if stop.load(Ordering::Relaxed) {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        // A planned brain-restart request (apply swapped the\n                        // binary, ADR-0018 D3-3): drop this brain so the respawn\n                        // lands on the new executable. A deliberate kill, NOT a\n                        // crash — stamp it `update`.\n                        if restart.take() {\n                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            planned = true;\n                            break;\n                        }\n                        match child.try_wait() {\n                            Ok(Some(status)) => {\n                                eprintln!(\"BRAIN_EXIT: brain child exited ({status}) — respawning\");\n                                break;\n                            }\n                            Ok(None) => thread::sleep(TICK),\n                            Err(e) => {\n                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            Err(e) => eprintln!(\"BRAIN_SPAWN_FAIL: {e}\"),\n        }\n        if stop.load(Ordering::Relaxed) {\n            return;\n        }\n        // Next spawn: advance the generation (every start, KH-2.4). A planned\n        // update respawns promptly with `start-reason=update` (it is not a crash,\n        // so the crash backoff does not apply); a crash recovery keeps the capped\n        // doubling backoff and stamps `crash`.\n        generation += 1;\n        if planned {\n            reason = StartReason::Update;\n            backoff = base;\n            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n            continue;\n        }\n        reason = StartReason::Crash;\n        backoff = next_backoff(backoff, started.elapsed(), base);\n        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n        sleep_backoff(backoff, stop);\n    }\n}\n\n/// Effect the rollback for an exhausted trial (D6-2): from the candidate's\n/// `AppliedPending` record, write `RolledBack{quarantine=N, running, rollback_binary}`\n/// and fire the loud notif (via `env`). `running` is the candidate's\n/// `prior_version` (the version actually running before the swap, captured by\n/// `apply`) — NOT `N-1`, which is wrong on a skip-version update (applied v5 →\n/// candidate v8: the `.old-8` aside holds v5 bytes, so `running` is v5, not v7).\n/// It degrades to `N-1` only for a pre-D6-2b record that lacks `prior_version`.\n/// A record that is not `AppliedPending` (already rolled back, or raced away) is\n/// a no-op — never a double rollback.\n///\n/// **Rollback is SELECTION, not an update (open-call-4).** The recovery never\n/// routes through the update engine / `apply` / `verify_metadata`: it just makes\n/// the supervisor *spawn the `.old-N` binary*. So the anti-downgrade monotonic\n/// version floor (`REQ-HAZARD-UPDATE-ROLLBACK` — *attack* protection that refuses\n/// a version *downgrade*) **never sees** this recovery, and the two concerns stay\n/// cleanly separate. Do NOT \"fix\" `verify_metadata` to permit recovery\n/// downgrades — selection-not-apply is the mechanism, by design.\nfn rollback(env: &dyn TrialEnv, record: &Option<AppliedRecord>) {\n    if let Some(AppliedRecord::AppliedPending {\n        version,\n        rollback_binary,\n        prior_version,\n        ..\n    }) = record\n    {\n        let running = prior_version.unwrap_or(version.saturating_sub(1));\n        eprintln!(\"BRAIN_ROLLBACK: update v{version} failed readiness — rolling back to v{running} ({rollback_binary}), quarantining v{version}\");\n        env.record_rolled_back(*version, running, rollback_binary);\n    }\n}\n\n/// Promotion bytes-gate verdict (KH 6.11, `REQ-HAZARD-BRAIN-RESPAWN-PATH`).\nenum BytesGate {\n    /// Candidate is running the staged bytes — promote.\n    Match,\n    /// Candidate is ready but running the WRONG bytes — fail the trial, roll back.\n    Mismatch,\n    /// Either hash absent (pre-metadata release / missing breadcrumb) — degrade\n    /// to readiness-only promotion (N-1-safe).\n    Unverified,\n}\n\n/// Compare the just-ready candidate's stamped `exe_hash` against the staged\n/// artifact hash for `version` on this platform (KH 6.11). Readiness alone is\n/// NOT proof the new bytes run — on Linux a brain respawned onto the renamed\n/// old binary still signals ready (the v0.4.1 kitsubito false-success). On\n/// either hash absent the gate degrades to readiness-only but emits\n/// `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n        (Some(_), Some(_)) => BytesGate::Mismatch,\n        (expected, _) => {\n            let which = if expected.is_none() {\n                \"artifact-hash-absent\"\n            } else {\n                \"ready-hash-absent\"\n            };\n            eprintln!(\"PROMOTE_BYTES_UNVERIFIED:{version}:{which}\");\n            BytesGate::Unverified\n        }\n    }\n}\n\n/// Spawn the production brain child: re-exec `daemon brain` as a plain\n/// (non-detached) managed child, so it inherits the broker's environment —\n/// including `SPT_HOME` — and dies with no orphaning. NOT `spawn_detached`\n/// (that path rebuilds the Windows env block and would drop `SPT_HOME`). The\n/// broker stamps `{generation, start-reason}` onto the argv (ADR-0018 V2).\n///\n/// `binary` is the **record-driven selection** (D6-2): `None` spawns the\n/// accepted / candidate binary (`canonical`, below), `Some(path)` spawns the\n/// rollback binary (the `.old-N` last-known-good) — the supervisor chooses, no\n/// file is renamed at the failure instant.\n///\n/// `canonical` is the canonical exe path captured ONCE at broker start\n/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n/// record-driven `.old-N` selection); else the `canonical` path captured at\n/// broker start; else — only if that capture failed — degrade to a per-spawn\n/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n/// when a canonical path is available, because on Linux `current_exe()` follows\n/// the `apply` rename to the old binary.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n    match binary {\n        Some(p) => Ok(p.to_path_buf()),\n        None => match canonical {\n            Some(c) => Ok(c.to_path_buf()),\n            None => std::env::current_exe(),\n        },\n    }\n}\n\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn spawn_brain_child(\n    generation: u64,\n    reason: StartReason,\n    binary: Option<&Path>,\n    canonical: Option<&Path>,\n) -> io::Result<Child> {\n    let exe = select_brain_exe(binary, canonical)?;\n    let mut cmd = Command::new(exe);\n    cmd.args(brain_child_args(generation, reason));\n    // The brain is a background logic process — it must never flash a console\n    // window. On Windows a plain console-subsystem spawn pops one whenever the\n    // broker has a console (a `spt daemon run` in a terminal, or any test that\n    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n    // the plain-child SPT_HOME inheritance (above) is preserved.\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n        cmd.creation_flags(CREATE_NO_WINDOW);\n    }\n    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n    // OWN process group so the daemon can reap the brain + everything it spawns\n    // (the shellwake watchers, the detached Psyches — all inherit this group) as\n    // one unit on stop, without signalling the daemon itself. Windows roots the\n    // reap on a Job object instead (assigned in the supervisor via BrainReaper).\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        unsafe {\n            cmd.pre_exec(|| {\n                // setpgid(0, 0): this child becomes the leader of a new group\n                // (pgid == its pid). Best-effort — a failure only loses the reap.\n                libc::setpgid(0, 0);\n                Ok(())\n            });\n        }\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n//! broker/brain process-isolation restoration, D1 skeleton).\n//!\n//! ADR-0004 specified the broker/brain split as two **processes**; production\n//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n//! the binary on disk but never restarts the running code (the regression\n//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n//! `spt daemon brain` child. The child connects over the existing broker socket\n//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n//! verb. The digest hub serve + seed-control stay broker-side continuity\n//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n//! loops.\n//!\n//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n//! broker spawns + supervises a brain child over the socket, and the broker\n//! (with its PTY children + seed channel) survives the brain dying — the\n//! supervisor respawns it from the executable path, which on a real update is\n//! the swapped binary.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\nuse std::io;\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::endpoint::{brain_ready_path, broker_socket_name};\nuse crate::relcache::{AppliedRecord, ReleaseCache};\n\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n/// never a hot respawn loop.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n/// A run at least this long counts as healthy and resets the backoff floor.\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n/// brain has this long to signal `ready` for its generation before the window\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n/// **exits before** signaling ready is respawned this many consecutive times\n/// (a transient first-boot hiccup should not abandon a good binary) before the\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n/// window-expiry triggers rollback.\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// How long the brain child retries connecting to the broker socket before\n/// giving up (the broker may still be binding — the child is freshly spawned).\nconst BRAIN_CONNECT_TRIES: u32 = 300;\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\n/// reliable than a brain→brain frame, which retires in D4).\n///\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\n/// never a parse failure that would brick the brain before it rehydrates.\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum StartReason {\n    /// The broker's first brain spawn this broker lifetime.\n    Cold,\n    /// A supervised respawn after an unexpected brain exit.\n    Crash,\n    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n    Update,\n}\n\nimpl StartReason {\n    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n    pub fn as_arg(self) -> &'static str {\n        match self {\n            StartReason::Cold => \"cold\",\n            StartReason::Crash => \"crash\",\n            StartReason::Update => \"update\",\n        }\n    }\n\n    /// Parse the `--start-reason` argv value, lenient: an unknown token (a\n    /// newer broker's reason an older brain predates) defaults to `Cold` rather\n    /// than failing the parse (KH-2.3 — defaults survive intermediate versions).\n    pub fn from_arg(s: &str) -> Self {\n        match s {\n            \"update\" => StartReason::Update,\n            \"crash\" => StartReason::Crash,\n            _ => StartReason::Cold,\n        }\n    }\n}\n\n/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n/// its next wait tick, kills the current brain child, and respawns it with\n/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n/// between the broker serve path and the supervisor — both run in the broker\n/// process. This is the seam that makes a routine `apply` actually run new code,\n/// the pillar the in-process-collapse regression silently unrealized.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n#[derive(Default)]\npub struct BrainRestart {\n    requested: AtomicBool,\n}\n\nimpl BrainRestart {\n    /// A fresh signal with no pending request.\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Request a planned restart (broker handler side) — idempotent: coalesces\n    /// with any already-pending request into one cycle.\n    pub fn request(&self) {\n        self.requested.store(true, Ordering::SeqCst);\n    }\n\n    /// Consume a pending request (supervisor side): true at most once per\n    /// `request`, clearing it so the next cycle starts fresh.\n    pub fn take(&self) -> bool {\n        self.requested.swap(false, Ordering::SeqCst)\n    }\n}\n\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n/// holding the connection and probing broker liveness each heartbeat. A dropped\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n/// from the (possibly swapped) binary.\n///\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n…\n    }\n}\n…\nfn run_trial(\n    child: &mut Child,\n    generation: u64,\n    window: Duration,\n    env: &dyn TrialEnv,\n    stop: &AtomicBool,\n) -> TrialStep {\n    let deadline = Instant::now() + window;\n    let mut ready_seen = false;\n    loop {\n        if stop.load(Ordering::Relaxed) {\n            return TrialStep::Stopped;\n        }\n        // Latch readiness FIRST (don't demote a ready-then-exit child) — but do NOT\n        // promote on it alone: the old generation's control plane must have drained.\n        if !ready_seen && env.ready_generation() == Some(generation) {\n            ready_seen = true;\n        }\n        if ready_seen && env.old_gen_drained() {\n            return TrialStep::Promoted;\n        }\n        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n        // once ready-seen we are waiting on the OLD gen to drain, and a ready-then-\n        // exited child still promotes (the exit is a normal later respawn).\n        if !ready_seen {\n            match child.try_wait() {\n                Ok(Some(_)) => return TrialStep::PreReadyExit,\n                Ok(None) => {}\n                // A wait error: treat as a pre-ready exit (fail toward retry, never\n                // a panic in the supervisor thread).\n                Err(e) => {\n                    eprintln!(\"BRAIN_TRIAL_WAIT_FAIL: {e}\");\n                    return TrialStep::PreReadyExit;\n                }\n            }\n        }\n        if Instant::now() >= deadline {\n            return TrialStep::WindowElapsedAlive;\n        }\n        thread::sleep(TICK);\n    }\n}\n\n/// Supervise the brain child: spawn it, wait for it, and respawn on exit after a\n/// capped, doubling backoff — until `stop` is raised, which kills the live child\n/// and returns. The process-level sibling of [`crate::pump::supervise_pump`]\n/// (ADR-0018 Q2: the broker is the always-up anchor that respawns the brain;\n/// Q3: a planned update is just the brain self-exiting on a path the supervisor\n/// already recovers from).\n///\n/// **Readiness-gated promotion + auto-rollback (ADR-0018 Q7, D6-2).** When the\n/// injected `env` reports an unpromoted `AppliedPending` record, the spawn is a\n/// **trial** (the latch is the *record*, **A8** — regardless of start reason):\n/// the supervisor clears `brain.ready` (**A9**), spawns the candidate, and waits\n/// for it to stamp ready for ITS generation within `window`. It **promotes** on\n/// ready (writes `Applied`, the candidate is now the accepted binary and a later\n/// crash is a normal respawn); it **rolls back** to the last-known-good binary\n/// on budget exhaustion — [`TRIAL_PREREADY_BUDGET`] consecutive pre-ready exits\n/// **or** one alive-but-never-ready window-expiry (which **kills** the candidate\n/// first, **A11** — never two live brains). Binary selection is **record-driven**\n/// (a `RolledBack` record spawns the good `.old-N` binary, surviving a reboot for\n/// free), so no file is renamed at the failure instant. Every hosted endpoint is\n/// untouched throughout — the broker holds them across the whole cycle.\n///\n/// Generic over `spawn_child` (which receives the **selected binary** — `None` =\n/// the default `current_exe`, `Some(path)` = the rollback binary) and `env`, so\n/// the unit harness injects fakes; production binds [`spawn_brain_child`] +\n/// [`ProductionTrialEnv`].\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn supervise_brain(\n    stop: &AtomicBool,\n    restart: &BrainRestart,\n    base: Duration,\n    env: &dyn TrialEnv,\n    window: Duration,\n    mut spawn_child: impl FnMut(u64, StartReason, Option<&Path>) -> io::Result<Child>,\n) {\n    let mut backoff = base;\n    // Broker-owned generation custody (ADR-0018 V2, KH-2.4): the counter lives in\n    // the never-restarting supervisor and increments on EVERY spawn (cold or\n    // respawn), so the broker — which observes every respawn — is the\n    // authoritative source, not the outgoing brain. The first spawn is `Cold`;\n    // every supervised respawn here is a `Crash` (an unexpected exit the\n    // supervisor recovers from). A *planned* update respawn stamps `Update`\n    // instead — wired in D3-3, where the apply trigger drives this loop.\n    let mut generation: u64 = 0;\n    let mut reason = StartReason::Cold;\n    // Consecutive pre-ready exits of the CURRENT trial candidate (A8 budget K).\n    // In-memory: a broker restart resets it — acceptable, the trial restarts.\n    let mut pre_ready_exits: u32 = 0;\n    while !stop.load(Ordering::Relaxed) {\n        let started = Instant::now();\n        // Record-driven binary selection + trial latch (A8). The latch is the\n        // unpromoted `AppliedPending` record, NOT the start reason; a `RolledBack`\n        // record selects the last-known-good binary (and is never a trial — it is\n        // the recovery steady state). A missing/corrupt record degrades to \"no\n        // trial, default binary\" — the supervisor never panics on the record.\n        let record = env.applied_state();\n        let binary: Option<PathBuf> = match &record {\n            Some(AppliedRecord::RolledBack { rollback_binary, .. }) => {\n                Some(PathBuf::from(rollback_binary))\n            }\n            _ => None,\n        };\n        let is_trial = matches!(record, Some(AppliedRecord::AppliedPending { .. }));\n        // A9: clear `brain.ready` immediately before a trial spawn — the prior\n        // child is already waited-on dead (below), so the only stamp that can\n        // exist post-clear is THIS trial child's. Closes the cross-lifetime hole\n        // that same-generation equality alone cannot (a stale gen-N file from a\n        // pre-reboot lifetime matching a fresh gen-N trial).\n        if is_trial {\n            env.clear_ready();\n        }\n        // Did this cycle end because of a planned update restart (Q3) rather than\n        // an unexpected crash? The broker — observing the cycle it initiated — is\n        // the authoritative source of that distinction (V2).\n        let mut planned = false;\n        match spawn_child(generation, reason, binary.as_deref()) {\n            Ok(mut child) => {\n                // The trial gate runs first for a candidate on trial; on\n                // promotion it falls through to the normal wait loop for the SAME\n                // child. A non-trial spawn skips straight to normal supervision.\n                let mut supervise = !is_trial;\n                if is_trial {\n                    match run_trial(&mut child, generation, window, env, stop) {\n                        TrialStep::Stopped => {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        TrialStep::Promoted => {\n                            // Bytes-gate (KH 6.11): readiness != new-bytes. Promote\n                            // only if the candidate is running the staged bytes; a\n                            // ready-but-wrong-bytes candidate (e.g. a Linux respawn\n                            // onto the renamed old binary) fails the trial → kill +\n                            // rollback, never a falsely-`applied` record.\n                            let version = match &record {\n                                Some(AppliedRecord::AppliedPending { version, .. }) => Some(*version),\n                                _ => None,\n                            };\n                            if let Some(version) = version {\n                                if let BytesGate::Mismatch = bytes_gate(env, version) {\n                                    eprintln!(\"BRAIN_PROMOTE_REJECTED: candidate v{version} ready but running the WRONG bytes (generation {generation}) — rolling back (KH 6.11)\");\n                                    let _ = child.kill();\n                                    let _ = child.wait();\n                                    rollback(env, &record);\n                                    pre_ready_exits = 0;\n                                    generation += 1;\n                                    reason = StartReason::Crash;\n                                    backoff = base;\n                                    continue;\n                                }\n                                eprintln!(\"BRAIN_PROMOTED: candidate v{version} signalled ready (generation {generation}) — swap accepted\");\n                                env.record_promoted(version);\n                            }\n                            pre_ready_exits = 0;\n                            backoff = base;\n                            supervise = true; // keep supervising the now-accepted child\n                        }\n                        TrialStep::PreReadyExit => {\n                            pre_ready_exits += 1;\n                            eprintln!(\"BRAIN_TRIAL_EXIT: candidate exited pre-ready ({pre_ready_exits}/{TRIAL_PREREADY_BUDGET})\");\n                            if pre_ready_exits >= TRIAL_PREREADY_BUDGET {\n                                rollback(env, &record);\n                                pre_ready_exits = 0;\n                                // The rollback binary is known-good — respawn it\n                                // promptly, no crash-penalty backoff (mirror the\n                                // WindowElapsedAlive path); only the under-budget\n                                // candidate retries keep the doubling backoff.\n                                backoff = base;\n                            } else {\n                                backoff = next_backoff(backoff, started.elapsed(), base);\n                            }\n                            generation += 1;\n                            reason = StartReason::Crash;\n                            sleep_backoff(backoff, stop);\n                            continue;\n                        }\n                        TrialStep::WindowElapsedAlive => {\n                            // A11: KILL the alive-but-never-ready candidate BEFORE\n                            // spawning the rollback binary — two live brains would\n                            // break every single-brain seed/socket/IPC assumption.\n                            eprintln!(\"BRAIN_TRIAL_TIMEOUT: candidate alive but never ready — killing before rollback\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            rollback(env, &record);\n                            pre_ready_exits = 0;\n                            generation += 1;\n                            // The rollback binary is known-good — respawn promptly,\n                            // no crash-penalty backoff.\n                            reason = StartReason::Crash;\n                            backoff = base;\n                            continue;\n                        }\n                    }\n                }\n                if supervise {\n                    loop {\n                        if stop.load(Ordering::Relaxed) {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        // A planned brain-restart request (apply swapped the\n                        // binary, ADR-0018 D3-3): drop this brain so the respawn\n                        // lands on the new executable. A deliberate kill, NOT a\n                        // crash — stamp it `update`.\n                        if restart.take() {\n                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            planned = true;\n                            break;\n                        }\n                        match child.try_wait() {\n                            Ok(Some(status)) => {\n                                eprintln!(\"BRAIN_EXIT: brain child exited ({status}) — respawning\");\n                                break;\n                            }\n                            Ok(None) => thread::sleep(TICK),\n                            Err(e) => {\n                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            Err(e) => eprintln!(\"BRAIN_SPAWN_FAIL: {e}\"),\n        }\n        if stop.load(Ordering::Relaxed) {\n            return;\n        }\n        // Next spawn: advance the generation (every start, KH-2.4). A planned\n        // update respawns promptly with `start-reason=update` (it is not a crash,\n        // so the crash backoff does not apply); a crash recovery keeps the capped\n        // doubling backoff and stamps `crash`.\n        generation += 1;\n        if planned {\n            reason = StartReason::Update;\n            backoff = base;\n            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n            continue;\n        }\n        reason = StartReason::Crash;\n        backoff = next_backoff(backoff, started.elapsed(), base);\n        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n        sleep_backoff(backoff, stop);\n    }\n}\n\n/// Effect the rollback for an exhausted trial (D6-2): from the candidate's\n/// `AppliedPending` record, write `RolledBack{quarantine=N, running, rollback_binary}`\n/// and fire the loud notif (via `env`). `running` is the candidate's\n/// `prior_version` (the version actually running before the swap, captured by\n/// `apply`) — NOT `N-1`, which is wrong on a skip-version update (applied v5 →\n/// candidate v8: the `.old-8` aside holds v5 bytes, so `running` is v5, not v7).\n/// It degrades to `N-1` only for a pre-D6-2b record that lacks `prior_version`.\n/// A record that is not `AppliedPending` (already rolled back, or raced away) is\n/// a no-op — never a double rollback.\n///\n/// **Rollback is SELECTION, not an update (open-call-4).** The recovery never\n/// routes through the update engine / `apply` / `verify_metadata`: it just makes\n/// the supervisor *spawn the `.old-N` binary*. So the anti-downgrade monotonic\n/// version floor (`REQ-HAZARD-UPDATE-ROLLBACK` — *attack* protection that refuses\n/// a version *downgrade*) **never sees** this recovery, and the two concerns stay\n/// cleanly separate. Do NOT \"fix\" `verify_metadata` to permit recovery\n/// downgrades — selection-not-apply is the mechanism, by design.\nfn rollback(env: &dyn TrialEnv, record: &Option<AppliedRecord>) {\n    if let Some(AppliedRecord::AppliedPending {\n        version,\n        rollback_binary,\n        prior_version,\n        ..\n    }) = record\n    {\n        let running = prior_version.unwrap_or(version.saturating_sub(1));\n        eprintln!(\"BRAIN_ROLLBACK: update v{version} failed readiness — rolling back to v{running} ({rollback_binary}), quarantining v{version}\");\n        env.record_rolled_back(*version, running, rollback_binary);\n    }\n}\n\n/// Promotion bytes-gate verdict (KH 6.11, `REQ-HAZARD-BRAIN-RESPAWN-PATH`).\nenum BytesGate {\n    /// Candidate is running the staged bytes — promote.\n    Match,\n    /// Candidate is ready but running the WRONG bytes — fail the trial, roll back.\n    Mismatch,\n    /// Either hash absent (pre-metadata release / missing breadcrumb) — degrade\n    /// to readiness-only promotion (N-1-safe).\n    Unverified,\n}\n\n/// Compare the just-ready candidate's stamped `exe_hash` against the staged\n/// artifact hash for `version` on this platform (KH 6.11). Readiness alone is\n/// NOT proof the new bytes run — on Linux a brain respawned onto the renamed\n/// old binary still signals ready (the v0.4.1 kitsubito false-success). On\n/// either hash absent the gate degrades to readiness-only but emits\n/// `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n        (Some(_), Some(_)) => BytesGate::Mismatch,\n        (expected, _) => {\n            let which = if expected.is_none() {\n                \"artifact-hash-absent\"\n            } else {\n                \"ready-hash-absent\"\n            };\n            eprintln!(\"PROMOTE_BYTES_UNVERIFIED:{version}:{which}\");\n            BytesGate::Unverified\n        }\n    }\n}\n\n/// Spawn the production brain child: re-exec `daemon brain` as a plain\n/// (non-detached) managed child, so it inherits the broker's environment —\n/// including `SPT_HOME` — and dies with no orphaning. NOT `spawn_detached`\n/// (that path rebuilds the Windows env block and would drop `SPT_HOME`). The\n/// broker stamps `{generation, start-reason}` onto the argv (ADR-0018 V2).\n///\n/// `binary` is the **record-driven selection** (D6-2): `None` spawns the\n/// accepted / candidate binary (`canonical`, below), `Some(path)` spawns the\n/// rollback binary (the `.old-N` last-known-good) — the supervisor chooses, no\n/// file is renamed at the failure instant.\n///\n/// `canonical` is the canonical exe path captured ONCE at broker start\n/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n/// record-driven `.old-N` selection); else the `canonical` path captured at\n/// broker start; else — only if that capture failed — degrade to a per-spawn\n/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n/// when a canonical path is available, because on Linux `current_exe()` follows\n/// the `apply` rename to the old binary.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n    match binary {\n        Some(p) => Ok(p.to_path_buf()),\n        None => match canonical {\n            Some(c) => Ok(c.to_path_buf()),\n            None => std::env::current_exe(),\n        },\n    }\n}\n\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn spawn_brain_child(\n    generation: u64,\n    reason: StartReason,\n    binary: Option<&Path>,\n    canonical: Option<&Path>,\n) -> io::Result<Child> {\n    let exe = select_brain_exe(binary, canonical)?;\n    let mut cmd = Command::new(exe);\n    cmd.args(brain_child_args(generation, reason));\n    // The brain is a background logic process — it must never flash a console\n    // window. On Windows a plain console-subsystem spawn pops one whenever the\n    // broker has a console (a `spt daemon run` in a terminal, or any test that\n    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n    // the plain-child SPT_HOME inheritance (above) is preserved.\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n        cmd.creation_flags(CREATE_NO_WINDOW);\n    }\n    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n    // OWN process group so the daemon can reap the brain + everything it spawns\n    // (the shellwake watchers, the detached Psyches — all inherit this group) as\n    // one unit on stop, without signalling the daemon itself. Windows roots the\n    // reap on a Job object instead (assigned in the supervisor via BrainReaper).\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        unsafe {\n            cmd.pre_exec(|| {\n                // setpgid(0, 0): this child becomes the leader of a new group\n                // (pgid == its pid). Best-effort — a failure only loses the reap.\n                libc::setpgid(0, 0);\n                Ok(())\n            });\n        }\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,null,298,299,null,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,null,1070]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"}}},"isError":false,"timestamp":1784195766451}}
{"type":"message","id":"24929429","parentId":"d4ec0359","timestamp":"2026-07-16T09:56:06.451Z","message":{"role":"toolResult","toolCallId":"call_RSGZXlfWD9s4sNBBGdUJ40Kg|fc_0cc7a0c9a797f232016a58aab65e64819ab9b6a0c6495512c9","toolName":"read","content":[{"type":"text","text":"//! The daemon process entrypoint + auto-start (B6).\n//!\n//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n//! of the process.\n//!\n//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`\n//! invocation calls it; if no daemon answers the seed-control ping it spawns\n//! `spt daemon` detached and waits (bounded) for it to come up. The reliable\n//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping\n//! against it is a true liveness test (`listen` is the interim model's anchor;\n//! here the seed channel plays that role for cold-start detection).\n// [impl->REQ-DAEMON-3]\n\nuse std::io;\n#[cfg(unix)]\nuse std::process::{Command, Stdio};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n\nuse crate::broker::{default_journal_path, Broker};\nuse crate::config::{DaemonConfig, RelayChoice};\nuse crate::digesthub::{self, DigestHub};\nuse crate::endpoint::{\n    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n};\nuse crate::nethost::{NetConfig, NetHost};\nuse crate::seedmap::{self, SeedRegistry};\n\n/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\nconst START_TIMEOUT: Duration = Duration::from_secs(5);\n/// Poll cadence while waiting for the spawned daemon to come up.\nconst START_POLL: Duration = Duration::from_millis(25);\n\n/// The per-machine broker/brain daemon process.\npub struct Daemon;\n\nimpl Daemon {\n    /// Run the daemon: bind the broker (served on a background thread) + the\n    /// seed-control channel (the foreground loop + liveness anchor) and serve\n    /// until the process is killed. The body of `spt daemon`.\n    pub fn run() -> io::Result<()> {\n        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n        // a daemon restarted from inside an agent session inherits that\n        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n        // identity that is NEVER correct daemon state and would otherwise\n        // leak into every spawn on the node. Scrub first, before any state\n        // is touched or any child can be spawned.\n        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n        spt_runtime::scrub_identity_env();\n        // Anchor the monotonic half of the stderr correlation stamps at\n        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n        crate::conn::init_log_anchor();\n        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n        // `spt daemon` (sudo / elevated shell) must not serve with the\n        // elevated token — its pipes would deny unelevated clients (Windows)\n        // or root the user's state universe (Linux). Unix drops privileges\n        // in-process before any state is touched; Windows cannot drop a\n        // token in-place, so it respawns de-elevated via the UAC linked\n        // token and exits (the child re-enters here unelevated). With no\n        // unelevated identity to drop to, serving continues as-is, loudly —\n        // a consistent universe, never a torn one.\n        // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n        // Drop target: the ELECTED default user when one exists, else the\n        // sudo invoker — electing it on first use (M8 decision 8,\n        // REQ-INSTALL-6: the first `sudo spt` decides; thereafter every\n        // elevated launch lands in the same account's universe).\n        // [impl->REQ-INSTALL-6]\n        #[cfg(unix)]\n        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n            match invoker.drop_in_process() {\n                Ok(()) => eprintln!(\"DEELEVATED: daemon dropped to uid {}\", invoker.uid),\n                // A drop target existed but the drop failed: serving as\n                // root would root the user's state universe — a known-torn\n                // state. ABORT (user-ratified 2026-06-06: refuse, don't limp).\n                Err(e) => {\n                    return Err(io::Error::other(format!(\n                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n                         run spt unelevated\"\n                    )));\n                }\n            }\n        }\n        #[cfg(windows)]\n        if crate::deelevate::is_elevated() {\n            let exe = std::env::current_exe()?;\n            match crate::deelevate::spawn_deelevated(\n                &exe.to_string_lossy(),\n                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n                // unelevated daemon then runs detach_console + the null-handles guard,\n                // so it never keeps live inherited stdio (matches every other rung).\n                &[\"daemon\".to_string(), \"run\".to_string(), \"--detached\".to_string()],\n            ) {\n                Ok(Some(pid)) => {\n                    eprintln!(\n                        \"DEELEVATED_RESPAWN: unelevated daemon pid {pid}; elevated copy exits\"\n                    );\n                    return Ok(());\n                }\n                // Uniformly-elevated universe (no unelevated identity):\n                // serving elevated is consistent there. Loud anyway.\n                Ok(None) => eprintln!(\n                    \"ELEVATED_DAEMON: no unelevated identity to drop to — serving with the \\\n                     elevated token\"\n                ),\n                // Target existed, respawn failed: an elevated daemon is a\n                // known-torn state. ABORT (user-ratified 2026-06-06).\n                Err(e) => {\n                    return Err(io::Error::other(format!(\n                        \"DEELEVATE_FAIL: {e} — refusing to serve with the elevated token; run \\\n                         spt from an unelevated shell\"\n                    )));\n                }\n            }\n        }\n\n        let reg = Arc::new(SeedRegistry::new());\n\n        // Boot housekeeping (KNOWN-HAZARDS 1.3): sweep stale `index.lock`\n        // wedges out of the git-touching runtime the daemon hosts — the\n        // context store's seed repo + its agent/project worktrees (a crashed\n…\n        // at boot — REQ-DAEMON-9) is transient and retried, but a broken\n        // identity is not, so we only arm the retry when identity is sound.\n        let node_hex: Option<String> = spt_store::nodeid::load_or_create()\n            .ok()\n            .map(|i| i.public_key().to_hex());\n        let net = if node_hex.is_some() {\n            try_start_net()\n        } else {\n            eprintln!(\"NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry\");\n            None\n        };\n\n        // The broker hosts PTYs (B1/B2) + the net endpoint (D4a). Serve it on a\n        // background thread; a bind failure there must not stop the seed channel\n        // (the B6 surface) — the PTY-hosting consumers arrive in B7/B8.\n        let net_up = net.is_some();\n        // Keep the broker handle alive past the bind so the brain supervisor's\n        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n        let broker_handle: Option<Arc<Broker>> =\n            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n                Ok(broker) => {\n                    let serve_broker = Arc::clone(&broker);\n                    thread::spawn(move || {\n                        let _ = serve_broker.serve();\n                    });\n                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n                // so they live with the restartable brain and respawn with it.\n                // The broker keeps only the NetHost bring-up and the boot-race\n                // self-heal that binds it; the brain polls `net-status` and\n                // starts the consumers once net reports enabled.\n                // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n                    if !net_up && node_hex.is_some() {\n                        // Boot-race self-heal (REQ-DAEMON-9): net failed to bind\n                        // but identity is sound — almost always the autostart\n                        // racing the network up at boot. Retry net in the\n                        // background and attach it to the broker when it\n                        // succeeds, so the node becomes reachable WITHOUT a\n                        // manual restart; the brain notices `net-status` flip\n                        // enabled and starts the consumers then.\n                        // [impl->REQ-DAEMON-9]\n                        let broker = Arc::clone(&broker);\n                        thread::spawn(move || net_retry_attach(broker));\n                    }\n                    // Endpoint autostart replay (F-038 W5, REQ-ENDPOINT-AUTOSTART):\n                    // bring saved startup defaults back up against the broker we\n                    // just bound. Background thread — a saved endpoint that fails\n                    // to come up logs loud and never blocks daemon start, the\n                    // seed channel, or the other replays.\n                    // [impl->REQ-ENDPOINT-AUTOSTART]\n                    thread::spawn(|| {\n                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n                    });\n                    // The node-local docs server (ADR-0036 §4): broker-side so\n                    // docs are up whenever the daemon is. Loopback only; a bind\n                    // failure (port squatted) logs loud and the daemon runs on —\n                    // docs are a degradable surface, never a boot blocker.\n                    // [impl->REQ-DOCS-LOCAL-SERVER]\n                    {\n                        let port = crate::docshost::resolve_docs_port(\n                            crate::config::DaemonConfig::load().docs_port,\n                            std::env::var(crate::docshost::DOCS_PORT_ENV).ok().as_deref(),\n                        );\n                        let docs_root = spt_store::perch::spt_home().join(\"docs\");\n                        match crate::docshost::start(docs_root, port) {\n                            Ok(bound) => eprintln!(\n                                \"DOCS_SERVER_UP: {}\",\n                                crate::docshost::docs_url(bound)\n                            ),\n                            Err(e) => eprintln!(\n                                \"DOCS_SERVER_BIND_FAIL: port {port}: {e} — docs surface \\\n                                 unavailable this run (daemon continues)\"\n                            ),\n                        }\n                    }\n                    Some(broker)\n                }\n                Err(e) => {\n                    eprintln!(\"BROKER_BIND_FAIL: {e}\");\n                    None\n                }\n            };\n\n        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n        // delta-stream subscribes against it. Served on a background thread like\n        // the broker. The per-session feed that *populates* the hub is driven by\n        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n        // sessions are spawned through the broker; a bare daemon serves the\n        // channel and answers \"no live digest\" until a session is hosted.\n        let digest_hub = Arc::new(DigestHub::new());\n        {\n            let hub = Arc::clone(&digest_hub);\n            thread::spawn(move || {\n                let _ = digesthub::serve_digest_control(&digest_socket_name(), hub);\n            });\n        }\n\n        // The drive hub (M11-W2, REQ-SHELL-3) holds the per-shell ephemeral drive\n        // slot — the owner→shell control surface. Broker-held like the digest hub\n        // (the daemon-lifetime CLI-reachable anchor; a brain restart loses\n        // nothing — the slot is ephemeral control, a restart correctly drops it).\n        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n        // [impl->REQ-SHELL-3]\n        let drive_hub = Arc::new(crate::drivehub::DriveHub::new());\n        {\n            let hub = Arc::clone(&drive_hub);\n            thread::spawn(move || {\n                let _ = crate::drivehub::serve_drive_control(&drive_socket_name(), hub);\n            });\n        }\n\n        // The per-shell shell-TUNNEL registry (M11-W3, REQ-SHELL-4) — the opaque\n        // reliable-ordered byte stream's identifier map. Broker-held like the drive\n        // hub (daemon-lifetime CLI-reachable anchor; in-memory so a restart correctly\n        // drops a dead link generation's stream ids). `spt shell tunnel` (owner) and\n        // the shell binary's `api tunnel` resolve their stream end through it.\n        // [impl->REQ-SHELL-4]\n        let tunnel_hub = Arc::new(crate::tunnelhub::TunnelHub::new());\n        // The relay needs the broker-owned NetHost (doyle gate A: broker-homed),\n        // so it serves only when the broker bound; a net-less daemon has no tunnel.\n        if let Some(broker) = &broker_handle {\n            let hub = Arc::clone(&tunnel_hub);\n            let broker = Arc::clone(broker);\n            thread::spawn(move || {\n                let _ = crate::tunnelhub::serve_tunnel_control(\n                    &crate::endpoint::tunnel_socket_name(),\n                    hub,\n                    broker,\n                );\n            });\n        }\n\n        // Shell wake-watchers (M5-D4b) — the offline half of the shell\n        // online/offline exclusivity — now run in the BRAIN child (restoration\n        // D2-2): shellwake is brain-owned logic (ADR-0018 Q5 exception), a pure\n        // disk-reconciler whose boot sweep re-reconciles the watcher children on\n        // every brain start. The broker no longer hosts it; see\n        // [`crate::brainproc::run_brain`].\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\n        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n        // restoration): the broker is the always-up per-machine anchor; it spawns\n        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\n        // makes an update a *planned* restart on this same path: the supervisor's\n        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n        // onto the new binary with `start-reason=update`. The stop flag is held\n        // for symmetry, never raised (the daemon runs until killed). The brain\n        // child inherits this process's already-unelevated token.\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n        // container the supervisor enrolls every (re)spawned brain into. Created\n        // here so it outlives the supervisor and is reachable from the\n        // graceful-stop path below. The brain subtree (the brain + its shellwake\n        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n        // never in the container.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        let reaper = crate::reap::BrainReaper::new();\n        // The supervisor stop flag — hoisted out of the wiring block (was held\n        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n        // it, so the supervisor does not respawn the brain we are about to reap.\n        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        {\n            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n                Arc::clone(&brain_stop),\n                Arc::clone(&reaper),\n                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n                // broker's live control-plane truth in-process; None on a bind-failed\n                // daemon degrades the gate to readiness-only.\n                broker_handle.clone(),\n            );\n            if let Some(broker) = &broker_handle {\n                broker.set_brain_restart(restart);\n            }\n        }\n\n        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness\n        // is the socket ping, never this file, so a stale value is harmless.\n        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n\n        // The seed-control channel is the foreground loop. Binding it is what\n        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n        // to start fails this bind and exits, leaving the winner serving. It\n        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);\n\n        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n        // does not respawn the brain we are about to kill) THEN reap the brain\n        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n        // spt.exe. The daemon is not in the reap container, so this never\n        // self-terminates: it returns cleanly below.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);\n        reaper.reap();\n        serve\n    }\n\n    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n    /// broker process spawns + supervises this; it connects to the broker over\n    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n    /// is parsed leniently ([`crate::brainproc::StartReason::from_arg`]) so an\n    /// unknown value from a newer broker defaults to `Cold` (KH-2.3).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn run_brain(generation: u64, start_reason: &str) -> io::Result<()> {\n        // Identity-env sanitize (F-036) — same first-thing scrub as the broker\n        // entry: the brain is normally broker-spawned (already clean), but a\n        // dev/manual `spt daemon brain` from inside a session must not carry\n        // that session's identity into the brain's role spawns either.\n        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n        spt_runtime::scrub_identity_env();\n        // Anchor the monotonic half of the stderr correlation stamps at\n        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n        crate::conn::init_log_anchor();\n        crate::brainproc::run_brain(\n            generation,\n            crate::brainproc::StartReason::from_arg(start_reason),\n        )\n    }\n}\n\n…\n/// passes either way.\nfn wait_until_up() -> io::Result<()> {\n    let mut waited = Duration::ZERO;\n    while waited < START_TIMEOUT {\n        if is_running() {\n            return Ok(());\n        }\n        thread::sleep(START_POLL);\n        waited += START_POLL;\n    }\n    Err(io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"daemon did not come up within the start window\",\n    ))\n}\n\n/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if\n/// one already answers; otherwise bring it up and wait (bounded) for the bind.\n///\n/// Service-aware (REQ-DAEMON-8): when an OS service manager has a registered\n/// spt-daemon, this starts THAT — never a competing manual `spawn_detached`\n/// daemon that would fight the service for the broker socket (the kitsubito\n/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n// [impl->REQ-DAEMON-8]\npub fn ensure_running() -> io::Result<()> {\n    if is_running() {\n        return Ok(());\n    }\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(false, svc.detected()) {\n        crate::service::StartPlan::ServiceStart => {\n            // Prefer the manager — but if it can't be driven (no session bus on\n            // a default-home box, manager not running), fall back to a manual\n            // daemon rather than leave the caller daemon-less. The autostart\n            // path must never hard-fail when a spawn would have worked.\n            if let Err(e) = svc.start() {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n            }\n        }\n        crate::service::StartPlan::ManualSpawn => spawn_detached()?,\n        // `running` was passed false, so AlreadyRunning is unreachable here.\n        crate::service::StartPlan::AlreadyRunning => return Ok(()),\n    }\n    wait_until_up()\n}\n\n/// Outcome of a service-aware `daemon start` (REQ-DAEMON-6), so the CLI can\n/// report truthfully what happened.\n#[derive(Debug)]\npub enum StartOutcome {\n    /// A daemon already answered — nothing started.\n    AlreadyRunning,\n    /// Started through the named service manager.\n    ServiceStarted(&'static str),\n    /// Spawned a manual detached daemon (no manager registered).\n    Spawned,\n}\n\n/// Outcome of a service-aware `daemon stop` (REQ-DAEMON-6).\n#[derive(Debug)]\npub enum StopOutcome {\n    /// Nothing was running — already stopped.\n    NotRunning,\n    /// Stopped through the named service manager.\n    ServiceStopped(&'static str),\n    /// Stopped a manual daemon over the seed-control IPC.\n    IpcStopped,\n}\n\n/// Service-aware `spt daemon start` (REQ-DAEMON-6): ensure the daemon is up in\n/// the background, preferring the registered manager; idempotent + non-blocking\n/// past the bind wait.\n// [impl->REQ-DAEMON-6]\npub fn start_daemon() -> io::Result<StartOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(is_running(), svc.detected()) {\n        crate::service::StartPlan::AlreadyRunning => Ok(StartOutcome::AlreadyRunning),\n        crate::service::StartPlan::ServiceStart => match svc.start() {\n            Ok(()) => {\n                wait_until_up()?;\n                Ok(StartOutcome::ServiceStarted(svc.label()))\n            }\n            // Manager present but undrivable (no session bus) — fall back to a\n            // manual daemon so `daemon start` still brings one up.\n            Err(e) => {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n                wait_until_up()?;\n                Ok(StartOutcome::Spawned)\n            }\n        },\n        crate::service::StartPlan::ManualSpawn => {\n            spawn_detached()?;\n            wait_until_up()?;\n            Ok(StartOutcome::Spawned)\n        }\n    }\n}\n\n/// Service-aware `spt daemon stop` (REQ-DAEMON-6): a registered + active\n/// service is stopped through its manager (so it does not auto-restart-fight);\n/// a manual daemon takes the graceful IPC stop; nothing running is already\n/// stopped. Idempotent.\n// [impl->REQ-DAEMON-6]\npub fn stop_daemon() -> io::Result<StopOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_stop(is_running(), svc.detected(), svc.is_active()) {\n        crate::service::StopPlan::NotRunning => Ok(StopOutcome::NotRunning),\n        crate::service::StopPlan::ServiceStop => {\n            svc.stop()?;\n            Ok(StopOutcome::ServiceStopped(svc.label()))\n        }\n        crate::service::StopPlan::IpcStop => {\n            request_stop()?;\n            Ok(StopOutcome::IpcStopped)\n        }\n    }\n}\n\n/// The label of a managed service that is BOTH registered and active — a\n/// conflict for an inline `daemon run` that is about to take the same socket\n/// (REQ-DAEMON-7). `None` when no manager would fight.\n// [impl->REQ-DAEMON-7]\npub fn active_service_conflict() -> Option<&'static str> {\n    let svc = crate::service::platform_service();\n    (svc.detected() && svc.is_active()).then(|| svc.label())\n}\n\n/// Spawn `spt daemon` as a detached background process. Inherits the environment\n/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical\n/// socket names — but drops the std handles so it outlives the launching CLI\n/// call. The child is independent (not killed when the parent exits): on unix it\n/// is reparented to init when the launcher exits; on Windows a child is not in\n/// the parent's job by default.\n///\n/// **Windows handle-inheritance guard (KNOWN-HAZARDS 5.6).** A caller may\n/// capture this process's stdout/stderr through a pipe (`Command::output()`, a\n/// harness hook reading the command's output). On Windows `CreateProcess` runs\n/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n/// pipe's write end and hold it open forever — the capturing reader never sees\n/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n/// (the first guard shipped here) still leaked a grandparent capture's pipe\n/// sitting in our handle table as a stray. So detached long-lived children are\n/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n/// `CLOEXEC`) and keeps the plain `Command` path.\n///\n/// **Elevation guard (KNOWN-HAZARDS 5.7).** The daemon must ALWAYS run\n/// unelevated in the invoking user's universe, whichever command spawns it:\n/// an elevated spawner (Windows UAC, `sudo`) de-elevates the child here —\n/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n/// no handle inheritance by construction, so 5.6 holds on that path too),\n/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n/// torn one.\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\nfn spawn_detached() -> io::Result<()> {\n    let exe = std::env::current_exe()?;\n    #[cfg(windows)]\n    {\n        let exe = exe.to_string_lossy();\n        // `--detached` (hidden) tells the spawned broker it was launched by the\n        // auto-start ladder → it FreeConsole()s its own console at startup\n        // (REQ-HAZARD-WMI-DAEMON-WINDOW). The WMI/schtasks rungs run the broker via\n        // a `cmd /c … start /b spt daemon run` wrapper; @15fdf58 detached the cmd\n        // WRAPPER, but the inner broker (a console-subsystem exe) still ALLOCATES its\n        // own conhost = a window. The flag rides every rung (spawn_deelevated +\n        // launch_daemon_job_neutral); a human-typed `spt daemon run` omits it and\n        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n        let args = [\n            \"daemon\".to_string(),\n            \"run\".to_string(),\n            \"--detached\".to_string(),\n        ];\n        match crate::deelevate::spawn_deelevated(&exe, &args) {\n            Ok(Some(_pid)) => return Ok(()), // de-elevated child running\n            Ok(None) => {\n                if crate::deelevate::is_elevated() {\n                    // No unelevated identity exists (UAC-off admin desktop,\n                    // SYSTEM): the whole universe is uniformly elevated, so\n                    // an elevated daemon is consistent there. Loud anyway.\n                    eprintln!(\n                        \"ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \\\n                         the elevated token\"\n                    );\n                }\n            }\n            // A de-elevation TARGET existed but the spawn failed: an\n            // elevated daemon here is a known-torn state (its pipes deny\n            // the unelevated clients that target exists for). ABORT — do\n            // not start a daemon that cannot serve (user-ratified\n            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,\n            // don't limp).\n            Err(e) => {\n                return Err(io::Error::other(format!(\n                    \"DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \\\n                     unelevated clients); run spt from an unelevated shell\"\n                )));\n            }\n        }\n        launch_daemon_job_neutral(&exe, &args)?;\n        Ok(())\n    }\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        let mut cmd = Command::new(exe);\n        cmd.args([\"daemon\", \"run\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::null())\n            .stderr(Stdio::null());\n        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n            // Drop the child to the elected default user (else the sudo\n            // invoker — M8 decision 8): std clears supplementary groups and\n            // orders setgid-before-setuid when uid is set.\n            // [impl->REQ-INSTALL-6]\n            invoker.stamp_env(&mut cmd);\n            cmd.uid(invoker.uid).gid(invoker.gid);\n        }\n        // setsid: detach into a NEW SESSION, not just a new process group —\n        // a daemon auto-started inside an ssh/non-interactive session\n        // otherwise dies with it (sshd HUPs the whole session on\n        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n        // the freshly-spawned daemon vanished the moment the ssh command\n        // returned). `process_group(0)` is NOT enough — the new group\n        // still belongs to the dying session. Safety: this closure only\n        // calls the async-signal-safe setsid(2).\n        unsafe {\n            cmd.pre_exec(|| {\n                if libc::setsid() == -1 {\n                    return Err(std::io::Error::last_os_error());\n                }\n                Ok(())\n            });\n        }\n        cmd.spawn()?;\n        Ok(())\n    }\n}\n\n/// CREATE_BREAKAWAY_FROM_JOB: escape the launching terminal's Job Object.\n/// Windows Terminal / VS Code put the launched shell AND every descendant in a\n/// Job with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the job's\n/// last handle → the OS terminates every member. A child created WITH this flag\n/// leaves the job at birth — but only if the job permits it (BREAKAWAY_OK /\n/// SILENT_BREAKAWAY_OK) or there is no job; a job that FORBIDS breakaway fails\n/// `CreateProcessW` with ERROR_ACCESS_DENIED. Used by [`detached_no_inherit`]'s\n/// fallback rung and, for the daemon, the [`launch_daemon_job_neutral`] ladder.\n#[cfg(windows)]\nconst CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;\n#[cfg(windows)]\nconst ERROR_ACCESS_DENIED: i32 = 5;\n\n/// One CreateProcessW attempt: spawn `program args…` detached, inheriting NO\n/// handles, with `extra_flags` OR'd onto the base detached flags. The single\n/// FFI primitive shared by [`detached_no_inherit`] and the daemon's job-neutral\n/// launcher ladder ([`launch_daemon_job_neutral`]).\n///\n/// **`bInheritHandles = FALSE`** is the KH 5.6 guard in its complete form: no\n/// handle (std or stray grandparent pipe) flows into the immortal child, so no\n/// capturing caller anywhere up the process chain can be wedged by it.\n/// `DETACHED_PROCESS` ⇒ no console, so the child's std handles are simply absent\n/// (every such child runs stdio-null by design anyway). `lpApplicationName`\n/// stays NULL so `program` resolves via PATH like `Command` would. Raw\n/// `CreateProcessW` declared inline to avoid a winapi dependency for one call.\n// [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]\n#[cfg(windows)]\npub(crate) fn create_process_detached(\n    program: &str,\n    args: &[String],\n    extra_flags: u32,\n) -> io::Result<u32> {\n    use std::os::windows::ffi::OsStrExt;\n\n    #[repr(C)]\n    struct StartupInfoW {\n        cb: u32,\n        reserved: *mut u16,\n        desktop: *mut u16,\n        title: *mut u16,\n        x: u32,\n        y: u32,\n        x_size: u32,\n        y_size: u32,\n        x_count_chars: u32,\n        y_count_chars: u32,\n        fill_attribute: u32,\n        flags: u32,\n        show_window: u16,\n        cb_reserved2: u16,\n        lp_reserved2: *mut u8,\n        std_input: isize,\n        std_output: isize,\n        std_error: isize,\n    }\n    #[repr(C)]\n    struct ProcessInformation {\n        process: isize,\n        thread: isize,\n        pid: u32,\n        tid: u32,\n    }\n    extern \"system\" {\n        fn CreateProcessW(\n            app: *const u16,\n            cmdline: *mut u16,\n            process_attrs: *mut core::ffi::c_void,\n            thread_attrs: *mut core::ffi::c_void,\n            inherit_handles: i32,\n            flags: u32,\n            env: *mut core::ffi::c_void,\n            cwd: *const u16,\n            startup: *mut StartupInfoW,\n            out: *mut ProcessInformation,\n        ) -> i32;\n        fn CloseHandle(h: isize) -> i32;\n    }\n    // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW: no\n    // inherited console, no popup window, its own signal group.\n    const BASE_FLAGS: u32 = 0x0000_0008 | 0x0000_0200 | 0x0800_0000;\n\n    let mut cmdline = String::new();\n    quote_windows_arg(&mut cmdline, program);\n    for a in args {\n        cmdline.push(' ');\n        quote_windows_arg(&mut cmdline, a);\n    }\n    // CreateProcessW may mutate the command-line buffer in place.\n    let mut cmdline_w: Vec<u16> = std::ffi::OsStr::new(&cmdline)\n        .encode_wide()\n        .chain(std::iter::once(0))\n        .collect();\n    let mut si: StartupInfoW = unsafe { std::mem::zeroed() };\n    si.cb = std::mem::size_of::<StartupInfoW>() as u32;\n    let mut pi: ProcessInformation = unsafe { std::mem::zeroed() };\n    let ok = unsafe {\n        CreateProcessW(\n            std::ptr::null(),\n            cmdline_w.as_mut_ptr(),\n            std::ptr::null_mut(),\n            std::ptr::null_mut(),\n            0, // bInheritHandles = FALSE — the whole point.\n            BASE_FLAGS | extra_flags,\n            std::ptr::null_mut(), // inherit this process's environment\n            std::ptr::null(),\n            &mut si,\n            &mut pi,\n        )\n    };\n    if ok == 0 {\n        return Err(io::Error::last_os_error());\n    }\n    unsafe {\n        CloseHandle(pi.process);\n        CloseHandle(pi.thread);\n    }\n    Ok(pi.pid)\n}\n\n/// Spawn `program args…` detached, breaking away from the launching Job Object\n/// when permitted, else in-job. Used by the shell binary\n/// ([`crate::shellhost::launch_shell`]) — a daemon-spawned shell is already\n/// job-neutral once the daemon itself is (see [`launch_daemon_job_neutral`]), so\n/// breakaway here is the simple best-effort form. The daemon's OWN cold-start no\n/// longer goes through this path; it uses the job-neutral ladder instead.\n// [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]\n#[cfg(windows)]\npub(crate) fn detached_no_inherit(program: &str, args: &[String]) -> io::Result<u32> {\n    // Try to break away first; if the launching Job Object forbids breakaway the\n    // call fails ACCESS_DENIED → retry in-job so we never regress a spawn that\n    // worked before (just can't escape that job; logged for diagnosis).\n    // [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n    match create_process_detached(program, args, CREATE_BREAKAWAY_FROM_JOB) {\n        Ok(pid) => Ok(pid),\n        Err(e) if e.raw_os_error() == Some(ERROR_ACCESS_DENIED) => {\n            eprintln!(\n                \"DETACH_BREAKAWAY_DENIED: launching Job Object forbids breakaway; \\\n                 spawned IN-JOB (may be reaped if the launching terminal closes)\"\n            );\n            create_process_detached(program, args, 0)\n        }\n        Err(e) => Err(e),\n    }\n}\n\n/// The job-neutral daemon-launch ladder, in first-success-wins order. Each rung\n/// is a distinct way to get the cold-started daemon OUT of the launching\n/// terminal's kill-on-close Job Object so a closed tab can never reap it.\n///\n/// The root cause this defeats: `spawn_detached` runs FROM the terminal-child\n/// CLI, so a daemon it spawns directly INHERITS the terminal's Job (DETACHED_PROCESS\n/// detaches the console, not the job). A job-neutral creator (WMI / Task\n/// Scheduler) makes the daemon a child of WmiPrvSE / the scheduler instead —\n/// outside any terminal job from birth, which is why Task-Scheduler-autostarted\n/// daemons never had this bug. Breakaway is demoted to a fallback because a job\n/// CAN deny it (the L1 finding).\n// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n#[cfg(windows)]\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum LaunchRung {\n    /// WMI `Win32_Process.Create` via absolute powershell — daemon = WmiPrvSE's\n    /// child, job-escaped regardless of the terminal job's breakaway policy.\n    Wmi,\n    /// `schtasks` one-shot — Task-Scheduler-owned, job-neutral.\n    Schtasks,\n    /// `CREATE_BREAKAWAY_FROM_JOB` — escapes only where the job permits it.\n    Breakaway,\n…\n}"}],"details":{"displayContent":{"text":"//! The daemon process entrypoint + auto-start (B6).\n//!\n//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n//! of the process.\n//!\n//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`\n//! invocation calls it; if no daemon answers the seed-control ping it spawns\n//! `spt daemon` detached and waits (bounded) for it to come up. The reliable\n//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping\n//! against it is a true liveness test (`listen` is the interim model's anchor;\n//! here the seed channel plays that role for cold-start detection).\n// [impl->REQ-DAEMON-3]\n\nuse std::io;\n#[cfg(unix)]\nuse std::process::{Command, Stdio};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n\nuse crate::broker::{default_journal_path, Broker};\nuse crate::config::{DaemonConfig, RelayChoice};\nuse crate::digesthub::{self, DigestHub};\nuse crate::endpoint::{\n    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n};\nuse crate::nethost::{NetConfig, NetHost};\nuse crate::seedmap::{self, SeedRegistry};\n\n/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\nconst START_TIMEOUT: Duration = Duration::from_secs(5);\n/// Poll cadence while waiting for the spawned daemon to come up.\nconst START_POLL: Duration = Duration::from_millis(25);\n\n/// The per-machine broker/brain daemon process.\npub struct Daemon;\n\nimpl Daemon {\n    /// Run the daemon: bind the broker (served on a background thread) + the\n    /// seed-control channel (the foreground loop + liveness anchor) and serve\n    /// until the process is killed. The body of `spt daemon`.\n    pub fn run() -> io::Result<()> {\n        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n        // a daemon restarted from inside an agent session inherits that\n        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n        // identity that is NEVER correct daemon state and would otherwise\n        // leak into every spawn on the node. Scrub first, before any state\n        // is touched or any child can be spawned.\n        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n        spt_runtime::scrub_identity_env();\n        // Anchor the monotonic half of the stderr correlation stamps at\n        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n        crate::conn::init_log_anchor();\n        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n        // `spt daemon` (sudo / elevated shell) must not serve with the\n        // elevated token — its pipes would deny unelevated clients (Windows)\n        // or root the user's state universe (Linux). Unix drops privileges\n        // in-process before any state is touched; Windows cannot drop a\n        // token in-place, so it respawns de-elevated via the UAC linked\n        // token and exits (the child re-enters here unelevated). With no\n        // unelevated identity to drop to, serving continues as-is, loudly —\n        // a consistent universe, never a torn one.\n        // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n        // Drop target: the ELECTED default user when one exists, else the\n        // sudo invoker — electing it on first use (M8 decision 8,\n        // REQ-INSTALL-6: the first `sudo spt` decides; thereafter every\n        // elevated launch lands in the same account's universe).\n        // [impl->REQ-INSTALL-6]\n        #[cfg(unix)]\n        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n            match invoker.drop_in_process() {\n                Ok(()) => eprintln!(\"DEELEVATED: daemon dropped to uid {}\", invoker.uid),\n                // A drop target existed but the drop failed: serving as\n                // root would root the user's state universe — a known-torn\n                // state. ABORT (user-ratified 2026-06-06: refuse, don't limp).\n                Err(e) => {\n                    return Err(io::Error::other(format!(\n                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n                         run spt unelevated\"\n                    )));\n                }\n            }\n        }\n        #[cfg(windows)]\n        if crate::deelevate::is_elevated() {\n            let exe = std::env::current_exe()?;\n            match crate::deelevate::spawn_deelevated(\n                &exe.to_string_lossy(),\n                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n                // unelevated daemon then runs detach_console + the null-handles guard,\n                // so it never keeps live inherited stdio (matches every other rung).\n                &[\"daemon\".to_string(), \"run\".to_string(), \"--detached\".to_string()],\n            ) {\n                Ok(Some(pid)) => {\n                    eprintln!(\n                        \"DEELEVATED_RESPAWN: unelevated daemon pid {pid}; elevated copy exits\"\n                    );\n                    return Ok(());\n                }\n                // Uniformly-elevated universe (no unelevated identity):\n                // serving elevated is consistent there. Loud anyway.\n                Ok(None) => eprintln!(\n                    \"ELEVATED_DAEMON: no unelevated identity to drop to — serving with the \\\n                     elevated token\"\n                ),\n                // Target existed, respawn failed: an elevated daemon is a\n                // known-torn state. ABORT (user-ratified 2026-06-06).\n                Err(e) => {\n                    return Err(io::Error::other(format!(\n                        \"DEELEVATE_FAIL: {e} — refusing to serve with the elevated token; run \\\n                         spt from an unelevated shell\"\n                    )));\n                }\n            }\n        }\n\n        let reg = Arc::new(SeedRegistry::new());\n\n        // Boot housekeeping (KNOWN-HAZARDS 1.3): sweep stale `index.lock`\n        // wedges out of the git-touching runtime the daemon hosts — the\n        // context store's seed repo + its agent/project worktrees (a crashed\n…\n        // at boot — REQ-DAEMON-9) is transient and retried, but a broken\n        // identity is not, so we only arm the retry when identity is sound.\n        let node_hex: Option<String> = spt_store::nodeid::load_or_create()\n            .ok()\n            .map(|i| i.public_key().to_hex());\n        let net = if node_hex.is_some() {\n            try_start_net()\n        } else {\n            eprintln!(\"NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry\");\n            None\n        };\n\n        // The broker hosts PTYs (B1/B2) + the net endpoint (D4a). Serve it on a\n        // background thread; a bind failure there must not stop the seed channel\n        // (the B6 surface) — the PTY-hosting consumers arrive in B7/B8.\n        let net_up = net.is_some();\n        // Keep the broker handle alive past the bind so the brain supervisor's\n        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n        let broker_handle: Option<Arc<Broker>> =\n            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n                Ok(broker) => {\n                    let serve_broker = Arc::clone(&broker);\n                    thread::spawn(move || {\n                        let _ = serve_broker.serve();\n                    });\n                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n                // so they live with the restartable brain and respawn with it.\n                // The broker keeps only the NetHost bring-up and the boot-race\n                // self-heal that binds it; the brain polls `net-status` and\n                // starts the consumers once net reports enabled.\n                // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n                    if !net_up && node_hex.is_some() {\n                        // Boot-race self-heal (REQ-DAEMON-9): net failed to bind\n                        // but identity is sound — almost always the autostart\n                        // racing the network up at boot. Retry net in the\n                        // background and attach it to the broker when it\n                        // succeeds, so the node becomes reachable WITHOUT a\n                        // manual restart; the brain notices `net-status` flip\n                        // enabled and starts the consumers then.\n                        // [impl->REQ-DAEMON-9]\n                        let broker = Arc::clone(&broker);\n                        thread::spawn(move || net_retry_attach(broker));\n                    }\n                    // Endpoint autostart replay (F-038 W5, REQ-ENDPOINT-AUTOSTART):\n                    // bring saved startup defaults back up against the broker we\n                    // just bound. Background thread — a saved endpoint that fails\n                    // to come up logs loud and never blocks daemon start, the\n                    // seed channel, or the other replays.\n                    // [impl->REQ-ENDPOINT-AUTOSTART]\n                    thread::spawn(|| {\n                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n                    });\n                    // The node-local docs server (ADR-0036 §4): broker-side so\n                    // docs are up whenever the daemon is. Loopback only; a bind\n                    // failure (port squatted) logs loud and the daemon runs on —\n                    // docs are a degradable surface, never a boot blocker.\n                    // [impl->REQ-DOCS-LOCAL-SERVER]\n                    {\n                        let port = crate::docshost::resolve_docs_port(\n                            crate::config::DaemonConfig::load().docs_port,\n                            std::env::var(crate::docshost::DOCS_PORT_ENV).ok().as_deref(),\n                        );\n                        let docs_root = spt_store::perch::spt_home().join(\"docs\");\n                        match crate::docshost::start(docs_root, port) {\n                            Ok(bound) => eprintln!(\n                                \"DOCS_SERVER_UP: {}\",\n                                crate::docshost::docs_url(bound)\n                            ),\n                            Err(e) => eprintln!(\n                                \"DOCS_SERVER_BIND_FAIL: port {port}: {e} — docs surface \\\n                                 unavailable this run (daemon continues)\"\n                            ),\n                        }\n                    }\n                    Some(broker)\n                }\n                Err(e) => {\n                    eprintln!(\"BROKER_BIND_FAIL: {e}\");\n                    None\n                }\n            };\n\n        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n        // delta-stream subscribes against it. Served on a background thread like\n        // the broker. The per-session feed that *populates* the hub is driven by\n        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n        // sessions are spawned through the broker; a bare daemon serves the\n        // channel and answers \"no live digest\" until a session is hosted.\n        let digest_hub = Arc::new(DigestHub::new());\n        {\n            let hub = Arc::clone(&digest_hub);\n            thread::spawn(move || {\n                let _ = digesthub::serve_digest_control(&digest_socket_name(), hub);\n            });\n        }\n\n        // The drive hub (M11-W2, REQ-SHELL-3) holds the per-shell ephemeral drive\n        // slot — the owner→shell control surface. Broker-held like the digest hub\n        // (the daemon-lifetime CLI-reachable anchor; a brain restart loses\n        // nothing — the slot is ephemeral control, a restart correctly drops it).\n        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n        // [impl->REQ-SHELL-3]\n        let drive_hub = Arc::new(crate::drivehub::DriveHub::new());\n        {\n            let hub = Arc::clone(&drive_hub);\n            thread::spawn(move || {\n                let _ = crate::drivehub::serve_drive_control(&drive_socket_name(), hub);\n            });\n        }\n\n        // The per-shell shell-TUNNEL registry (M11-W3, REQ-SHELL-4) — the opaque\n        // reliable-ordered byte stream's identifier map. Broker-held like the drive\n        // hub (daemon-lifetime CLI-reachable anchor; in-memory so a restart correctly\n        // drops a dead link generation's stream ids). `spt shell tunnel` (owner) and\n        // the shell binary's `api tunnel` resolve their stream end through it.\n        // [impl->REQ-SHELL-4]\n        let tunnel_hub = Arc::new(crate::tunnelhub::TunnelHub::new());\n        // The relay needs the broker-owned NetHost (doyle gate A: broker-homed),\n        // so it serves only when the broker bound; a net-less daemon has no tunnel.\n        if let Some(broker) = &broker_handle {\n            let hub = Arc::clone(&tunnel_hub);\n            let broker = Arc::clone(broker);\n            thread::spawn(move || {\n                let _ = crate::tunnelhub::serve_tunnel_control(\n                    &crate::endpoint::tunnel_socket_name(),\n                    hub,\n                    broker,\n                );\n            });\n        }\n\n        // Shell wake-watchers (M5-D4b) — the offline half of the shell\n        // online/offline exclusivity — now run in the BRAIN child (restoration\n        // D2-2): shellwake is brain-owned logic (ADR-0018 Q5 exception), a pure\n        // disk-reconciler whose boot sweep re-reconciles the watcher children on\n        // every brain start. The broker no longer hosts it; see\n        // [`crate::brainproc::run_brain`].\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\n        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n        // restoration): the broker is the always-up per-machine anchor; it spawns\n        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\n        // makes an update a *planned* restart on this same path: the supervisor's\n        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n        // onto the new binary with `start-reason=update`. The stop flag is held\n        // for symmetry, never raised (the daemon runs until killed). The brain\n        // child inherits this process's already-unelevated token.\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n        // container the supervisor enrolls every (re)spawned brain into. Created\n        // here so it outlives the supervisor and is reachable from the\n        // graceful-stop path below. The brain subtree (the brain + its shellwake\n        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n        // never in the container.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        let reaper = crate::reap::BrainReaper::new();\n        // The supervisor stop flag — hoisted out of the wiring block (was held\n        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n        // it, so the supervisor does not respawn the brain we are about to reap.\n        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        {\n            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n                Arc::clone(&brain_stop),\n                Arc::clone(&reaper),\n                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n                // broker's live control-plane truth in-process; None on a bind-failed\n                // daemon degrades the gate to readiness-only.\n                broker_handle.clone(),\n            );\n            if let Some(broker) = &broker_handle {\n                broker.set_brain_restart(restart);\n            }\n        }\n\n        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness\n        // is the socket ping, never this file, so a stale value is harmless.\n        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n\n        // The seed-control channel is the foreground loop. Binding it is what\n        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n        // to start fails this bind and exits, leaving the winner serving. It\n        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);\n\n        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n        // does not respawn the brain we are about to kill) THEN reap the brain\n        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n        // spt.exe. The daemon is not in the reap container, so this never\n        // self-terminates: it returns cleanly below.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);\n        reaper.reap();\n        serve\n    }\n\n    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n    /// broker process spawns + supervises this; it connects to the broker over\n    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n    /// is parsed leniently ([`crate::brainproc::StartReason::from_arg`]) so an\n    /// unknown value from a newer broker defaults to `Cold` (KH-2.3).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn run_brain(generation: u64, start_reason: &str) -> io::Result<()> {\n        // Identity-env sanitize (F-036) — same first-thing scrub as the broker\n        // entry: the brain is normally broker-spawned (already clean), but a\n        // dev/manual `spt daemon brain` from inside a session must not carry\n        // that session's identity into the brain's role spawns either.\n        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n        spt_runtime::scrub_identity_env();\n        // Anchor the monotonic half of the stderr correlation stamps at\n        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n        crate::conn::init_log_anchor();\n        crate::brainproc::run_brain(\n            generation,\n            crate::brainproc::StartReason::from_arg(start_reason),\n        )\n    }\n}\n\n…\n/// passes either way.\nfn wait_until_up() -> io::Result<()> {\n    let mut waited = Duration::ZERO;\n    while waited < START_TIMEOUT {\n        if is_running() {\n            return Ok(());\n        }\n        thread::sleep(START_POLL);\n        waited += START_POLL;\n    }\n    Err(io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"daemon did not come up within the start window\",\n    ))\n}\n\n/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if\n/// one already answers; otherwise bring it up and wait (bounded) for the bind.\n///\n/// Service-aware (REQ-DAEMON-8): when an OS service manager has a registered\n/// spt-daemon, this starts THAT — never a competing manual `spawn_detached`\n/// daemon that would fight the service for the broker socket (the kitsubito\n/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n// [impl->REQ-DAEMON-8]\npub fn ensure_running() -> io::Result<()> {\n    if is_running() {\n        return Ok(());\n    }\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(false, svc.detected()) {\n        crate::service::StartPlan::ServiceStart => {\n            // Prefer the manager — but if it can't be driven (no session bus on\n            // a default-home box, manager not running), fall back to a manual\n            // daemon rather than leave the caller daemon-less. The autostart\n            // path must never hard-fail when a spawn would have worked.\n            if let Err(e) = svc.start() {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n            }\n        }\n        crate::service::StartPlan::ManualSpawn => spawn_detached()?,\n        // `running` was passed false, so AlreadyRunning is unreachable here.\n        crate::service::StartPlan::AlreadyRunning => return Ok(()),\n    }\n    wait_until_up()\n}\n\n/// Outcome of a service-aware `daemon start` (REQ-DAEMON-6), so the CLI can\n/// report truthfully what happened.\n#[derive(Debug)]\npub enum StartOutcome {\n    /// A daemon already answered — nothing started.\n    AlreadyRunning,\n    /// Started through the named service manager.\n    ServiceStarted(&'static str),\n    /// Spawned a manual detached daemon (no manager registered).\n    Spawned,\n}\n\n/// Outcome of a service-aware `daemon stop` (REQ-DAEMON-6).\n#[derive(Debug)]\npub enum StopOutcome {\n    /// Nothing was running — already stopped.\n    NotRunning,\n    /// Stopped through the named service manager.\n    ServiceStopped(&'static str),\n    /// Stopped a manual daemon over the seed-control IPC.\n    IpcStopped,\n}\n\n/// Service-aware `spt daemon start` (REQ-DAEMON-6): ensure the daemon is up in\n/// the background, preferring the registered manager; idempotent + non-blocking\n/// past the bind wait.\n// [impl->REQ-DAEMON-6]\npub fn start_daemon() -> io::Result<StartOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(is_running(), svc.detected()) {\n        crate::service::StartPlan::AlreadyRunning => Ok(StartOutcome::AlreadyRunning),\n        crate::service::StartPlan::ServiceStart => match svc.start() {\n            Ok(()) => {\n                wait_until_up()?;\n                Ok(StartOutcome::ServiceStarted(svc.label()))\n            }\n            // Manager present but undrivable (no session bus) — fall back to a\n            // manual daemon so `daemon start` still brings one up.\n            Err(e) => {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n                wait_until_up()?;\n                Ok(StartOutcome::Spawned)\n            }\n        },\n        crate::service::StartPlan::ManualSpawn => {\n            spawn_detached()?;\n            wait_until_up()?;\n            Ok(StartOutcome::Spawned)\n        }\n    }\n}\n\n/// Service-aware `spt daemon stop` (REQ-DAEMON-6): a registered + active\n/// service is stopped through its manager (so it does not auto-restart-fight);\n/// a manual daemon takes the graceful IPC stop; nothing running is already\n/// stopped. Idempotent.\n// [impl->REQ-DAEMON-6]\npub fn stop_daemon() -> io::Result<StopOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_stop(is_running(), svc.detected(), svc.is_active()) {\n        crate::service::StopPlan::NotRunning => Ok(StopOutcome::NotRunning),\n        crate::service::StopPlan::ServiceStop => {\n            svc.stop()?;\n            Ok(StopOutcome::ServiceStopped(svc.label()))\n        }\n        crate::service::StopPlan::IpcStop => {\n            request_stop()?;\n            Ok(StopOutcome::IpcStopped)\n        }\n    }\n}\n\n/// The label of a managed service that is BOTH registered and active — a\n/// conflict for an inline `daemon run` that is about to take the same socket\n/// (REQ-DAEMON-7). `None` when no manager would fight.\n// [impl->REQ-DAEMON-7]\npub fn active_service_conflict() -> Option<&'static str> {\n    let svc = crate::service::platform_service();\n    (svc.detected() && svc.is_active()).then(|| svc.label())\n}\n\n/// Spawn `spt daemon` as a detached background process. Inherits the environment\n/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical\n/// socket names — but drops the std handles so it outlives the launching CLI\n/// call. The child is independent (not killed when the parent exits): on unix it\n/// is reparented to init when the launcher exits; on Windows a child is not in\n/// the parent's job by default.\n///\n/// **Windows handle-inheritance guard (KNOWN-HAZARDS 5.6).** A caller may\n/// capture this process's stdout/stderr through a pipe (`Command::output()`, a\n/// harness hook reading the command's output). On Windows `CreateProcess` runs\n/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n/// pipe's write end and hold it open forever — the capturing reader never sees\n/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n/// (the first guard shipped here) still leaked a grandparent capture's pipe\n/// sitting in our handle table as a stray. So detached long-lived children are\n/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n/// `CLOEXEC`) and keeps the plain `Command` path.\n///\n/// **Elevation guard (KNOWN-HAZARDS 5.7).** The daemon must ALWAYS run\n/// unelevated in the invoking user's universe, whichever command spawns it:\n/// an elevated spawner (Windows UAC, `sudo`) de-elevates the child here —\n/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n/// no handle inheritance by construction, so 5.6 holds on that path too),\n/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n/// torn one.\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\nfn spawn_detached() -> io::Result<()> {\n    let exe = std::env::current_exe()?;\n    #[cfg(windows)]\n    {\n        let exe = exe.to_string_lossy();\n        // `--detached` (hidden) tells the spawned broker it was launched by the\n        // auto-start ladder → it FreeConsole()s its own console at startup\n        // (REQ-HAZARD-WMI-DAEMON-WINDOW). The WMI/schtasks rungs run the broker via\n        // a `cmd /c … start /b spt daemon run` wrapper; @15fdf58 detached the cmd\n        // WRAPPER, but the inner broker (a console-subsystem exe) still ALLOCATES its\n        // own conhost = a window. The flag rides every rung (spawn_deelevated +\n        // launch_daemon_job_neutral); a human-typed `spt daemon run` omits it and\n        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n        let args = [\n            \"daemon\".to_string(),\n            \"run\".to_string(),\n            \"--detached\".to_string(),\n        ];\n        match crate::deelevate::spawn_deelevated(&exe, &args) {\n            Ok(Some(_pid)) => return Ok(()), // de-elevated child running\n            Ok(None) => {\n                if crate::deelevate::is_elevated() {\n                    // No unelevated identity exists (UAC-off admin desktop,\n                    // SYSTEM): the whole universe is uniformly elevated, so\n                    // an elevated daemon is consistent there. Loud anyway.\n                    eprintln!(\n                        \"ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \\\n                         the elevated token\"\n                    );\n                }\n            }\n            // A de-elevation TARGET existed but the spawn failed: an\n            // elevated daemon here is a known-torn state (its pipes deny\n            // the unelevated clients that target exists for). ABORT — do\n            // not start a daemon that cannot serve (user-ratified\n            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,\n            // don't limp).\n            Err(e) => {\n                return Err(io::Error::other(format!(\n                    \"DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \\\n                     unelevated clients); run spt from an unelevated shell\"\n                )));\n            }\n        }\n        launch_daemon_job_neutral(&exe, &args)?;\n        Ok(())\n    }\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        let mut cmd = Command::new(exe);\n        cmd.args([\"daemon\", \"run\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::null())\n            .stderr(Stdio::null());\n        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n            // Drop the child to the elected default user (else the sudo\n            // invoker — M8 decision 8): std clears supplementary groups and\n            // orders setgid-before-setuid when uid is set.\n            // [impl->REQ-INSTALL-6]\n            invoker.stamp_env(&mut cmd);\n            cmd.uid(invoker.uid).gid(invoker.gid);\n        }\n        // setsid: detach into a NEW SESSION, not just a new process group —\n        // a daemon auto-started inside an ssh/non-interactive session\n        // otherwise dies with it (sshd HUPs the whole session on\n        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n        // the freshly-spawned daemon vanished the moment the ssh command\n        // returned). `process_group(0)` is NOT enough — the new group\n        // still belongs to the dying session. Safety: this closure only\n        // calls the async-signal-safe setsid(2).\n        unsafe {\n            cmd.pre_exec(|| {\n                if libc::setsid() == -1 {\n                    return Err(std::io::Error::last_os_error());\n                }\n                Ok(())\n            });\n        }\n        cmd.spawn()?;\n        Ok(())\n    }\n}\n\n/// CREATE_BREAKAWAY_FROM_JOB: escape the launching terminal's Job Object.\n/// Windows Terminal / VS Code put the launched shell AND every descendant in a\n/// Job with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the job's\n/// last handle → the OS terminates every member. A child created WITH this flag\n/// leaves the job at birth — but only if the job permits it (BREAKAWAY_OK /\n/// SILENT_BREAKAWAY_OK) or there is no job; a job that FORBIDS breakaway fails\n/// `CreateProcessW` with ERROR_ACCESS_DENIED. Used by [`detached_no_inherit`]'s\n/// fallback rung and, for the daemon, the [`launch_daemon_job_neutral`] ladder.\n#[cfg(windows)]\nconst CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;\n#[cfg(windows)]\nconst ERROR_ACCESS_DENIED: i32 = 5;\n\n/// One CreateProcessW attempt: spawn `program args…` detached, inheriting NO\n/// handles, with `extra_flags` OR'd onto the base detached flags. The single\n/// FFI primitive shared by [`detached_no_inherit`] and the daemon's job-neutral\n/// launcher ladder ([`launch_daemon_job_neutral`]).\n///\n/// **`bInheritHandles = FALSE`** is the KH 5.6 guard in its complete form: no\n/// handle (std or stray grandparent pipe) flows into the immortal child, so no\n/// capturing caller anywhere up the process chain can be wedged by it.\n/// `DETACHED_PROCESS` ⇒ no console, so the child's std handles are simply absent\n/// (every such child runs stdio-null by design anyway). `lpApplicationName`\n/// stays NULL so `program` resolves via PATH like `Command` would. Raw\n/// `CreateProcessW` declared inline to avoid a winapi dependency for one call.\n// [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]\n#[cfg(windows)]\npub(crate) fn create_process_detached(\n    program: &str,\n    args: &[String],\n    extra_flags: u32,\n) -> io::Result<u32> {\n    use std::os::windows::ffi::OsStrExt;\n\n    #[repr(C)]\n    struct StartupInfoW {\n        cb: u32,\n        reserved: *mut u16,\n        desktop: *mut u16,\n        title: *mut u16,\n        x: u32,\n        y: u32,\n        x_size: u32,\n        y_size: u32,\n        x_count_chars: u32,\n        y_count_chars: u32,\n        fill_attribute: u32,\n        flags: u32,\n        show_window: u16,\n        cb_reserved2: u16,\n        lp_reserved2: *mut u8,\n        std_input: isize,\n        std_output: isize,\n        std_error: isize,\n    }\n    #[repr(C)]\n    struct ProcessInformation {\n        process: isize,\n        thread: isize,\n        pid: u32,\n        tid: u32,\n    }\n    extern \"system\" {\n        fn CreateProcessW(\n            app: *const u16,\n            cmdline: *mut u16,\n            process_attrs: *mut core::ffi::c_void,\n            thread_attrs: *mut core::ffi::c_void,\n            inherit_handles: i32,\n            flags: u32,\n            env: *mut core::ffi::c_void,\n            cwd: *const u16,\n            startup: *mut StartupInfoW,\n            out: *mut ProcessInformation,\n        ) -> i32;\n        fn CloseHandle(h: isize) -> i32;\n    }\n    // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW: no\n    // inherited console, no popup window, its own signal group.\n    const BASE_FLAGS: u32 = 0x0000_0008 | 0x0000_0200 | 0x0800_0000;\n\n    let mut cmdline = String::new();\n    quote_windows_arg(&mut cmdline, program);\n    for a in args {\n        cmdline.push(' ');\n        quote_windows_arg(&mut cmdline, a);\n    }\n    // CreateProcessW may mutate the command-line buffer in place.\n    let mut cmdline_w: Vec<u16> = std::ffi::OsStr::new(&cmdline)\n        .encode_wide()\n        .chain(std::iter::once(0))\n        .collect();\n    let mut si: StartupInfoW = unsafe { std::mem::zeroed() };\n    si.cb = std::mem::size_of::<StartupInfoW>() as u32;\n    let mut pi: ProcessInformation = unsafe { std::mem::zeroed() };\n    let ok = unsafe {\n        CreateProcessW(\n            std::ptr::null(),\n            cmdline_w.as_mut_ptr(),\n            std::ptr::null_mut(),\n            std::ptr::null_mut(),\n            0, // bInheritHandles = FALSE — the whole point.\n            BASE_FLAGS | extra_flags,\n            std::ptr::null_mut(), // inherit this process's environment\n            std::ptr::null(),\n            &mut si,\n            &mut pi,\n        )\n    };\n    if ok == 0 {\n        return Err(io::Error::last_os_error());\n    }\n    unsafe {\n        CloseHandle(pi.process);\n        CloseHandle(pi.thread);\n    }\n    Ok(pi.pid)\n}\n\n/// Spawn `program args…` detached, breaking away from the launching Job Object\n/// when permitted, else in-job. Used by the shell binary\n/// ([`crate::shellhost::launch_shell`]) — a daemon-spawned shell is already\n/// job-neutral once the daemon itself is (see [`launch_daemon_job_neutral`]), so\n/// breakaway here is the simple best-effort form. The daemon's OWN cold-start no\n/// longer goes through this path; it uses the job-neutral ladder instead.\n// [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]\n#[cfg(windows)]\npub(crate) fn detached_no_inherit(program: &str, args: &[String]) -> io::Result<u32> {\n    // Try to break away first; if the launching Job Object forbids breakaway the\n    // call fails ACCESS_DENIED → retry in-job so we never regress a spawn that\n    // worked before (just can't escape that job; logged for diagnosis).\n    // [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n    match create_process_detached(program, args, CREATE_BREAKAWAY_FROM_JOB) {\n        Ok(pid) => Ok(pid),\n        Err(e) if e.raw_os_error() == Some(ERROR_ACCESS_DENIED) => {\n            eprintln!(\n                \"DETACH_BREAKAWAY_DENIED: launching Job Object forbids breakaway; \\\n                 spawned IN-JOB (may be reaped if the launching terminal closes)\"\n            );\n            create_process_detached(program, args, 0)\n        }\n        Err(e) => Err(e),\n    }\n}\n\n/// The job-neutral daemon-launch ladder, in first-success-wins order. Each rung\n/// is a distinct way to get the cold-started daemon OUT of the launching\n/// terminal's kill-on-close Job Object so a closed tab can never reap it.\n///\n/// The root cause this defeats: `spawn_detached` runs FROM the terminal-child\n/// CLI, so a daemon it spawns directly INHERITS the terminal's Job (DETACHED_PROCESS\n/// detaches the console, not the job). A job-neutral creator (WMI / Task\n/// Scheduler) makes the daemon a child of WmiPrvSE / the scheduler instead —\n/// outside any terminal job from birth, which is why Task-Scheduler-autostarted\n/// daemons never had this bug. Breakaway is demoted to a fallback because a job\n/// CAN deny it (the L1 finding).\n// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n#[cfg(windows)]\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum LaunchRung {\n    /// WMI `Win32_Process.Create` via absolute powershell — daemon = WmiPrvSE's\n    /// child, job-escaped regardless of the terminal job's breakaway policy.\n    Wmi,\n    /// `schtasks` one-shot — Task-Scheduler-owned, job-neutral.\n    Schtasks,\n    /// `CREATE_BREAKAWAY_FROM_JOB` — escapes only where the job permits it.\n    Breakaway,\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,null,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,null,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,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,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,null,893]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\daemon.rs"}}},"isError":false,"timestamp":1784195766451}}
{"type":"message","id":"fa071f69","parentId":"24929429","timestamp":"2026-07-16T09:56:06.457Z","message":{"role":"toolResult","toolCallId":"call_kWiJ5Qi6nT56uJ3o35VhPilH|fc_0cc7a0c9a797f232016a58aab65e68819abd255d7be94140f3","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/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 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 366|    brain\n*367|        .request_brain_restart()\n 368|        .map_err(|e| format!(\"brain-restart trigger: {e}\"))\n 369|}\n 370|\n...\n 792|    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n*793|    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n 794|    // supervisor — the test/no-daemon shape) accepts the verb but acks\n 795|    // `honored: false`. This is the broker half of the apply trigger that\n 796|    // replaces the in-process handoff.\n...\n 804|        assert!(\n*805|            !b.request_brain_restart().expect(\"verb round-trips\"),\n 806|            \"a broker with no supervisor wired must ack honored=false\"\n 807|        );\n 808|\n...\n 810|        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*811|        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 812|        let mut b2 = cold_connect_retry(&name);\n 813|        assert!(\n*814|            b2.request_brain_restart().expect(\"verb round-trips\"),\n 815|            \"a broker with a supervisor wired must ack honored=true\"\n 816|        );\n 817|        assert!(\n...\n 841|        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*842|        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 843|        assert!(\n 844|            refresh_brain(&name).expect(\"refresh round-trips\"),\n 845|            \"a wired supervisor must report honored=true\"\n## brain.rs\n 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 45|    ViewerEvictedEvent,\n*46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n*47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n 48|    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n 49|    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n 50|    KIND_NET_SENT,\n...\n 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 106|/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n*107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n 108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n 110|/// between the broker serve path and the supervisor — both run in the broker\n...\n 1092|/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n*1093|/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n 1094|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1095|pub fn spawn_brain_supervisor(\n 1096|    stop: Arc<AtomicBool>,\n...\n 1205|    /// The planned-restart path (D3-3, Q3): a `BrainRestart::request` (the\n*1206|    /// broker's `KIND_BRAIN_RESTART` handler) makes the supervisor cycle the\n 1207|    /// brain and stamp the next spawn `Update` — distinguishing an intentional\n 1208|    /// update cycle from a crash (V2, broker-authoritative). A long-lived child\n 1209|    /// is used so the restart, not an exit, is what ends the cycle.\n## broker.rs\n 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*60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62|    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63|    KIND_NET_SENT,\n...\n 2541|    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542|    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543|    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n 2544|    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n 2545|    /// `Daemon::run` after it spawns the supervisor.\n...\n 2704|    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n*2705|    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n 2706|    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707|    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n 2708|    /// reports `honored: false` (no supervisor to respawn the child).\n 2709|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*2710|    pub fn set_brain_restart(&self, signal: Arc<crate::brainproc::BrainRestart>) -> bool {\n 2711|        self.brain_restart.set(signal).is_ok()\n 2712|    }\n 2713|\n...\n 3190|                }\n*3191|                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192|                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193|                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194|            }\n...\n 3893|        let frame = Envelope::new(\n*3894|            KIND_BRAIN_RESTARTED,\n*3895|            serde_json::to_value(BrainRestarted { honored })\n*3896|                .expect(\"BrainRestarted serializes\"),\n 3897|        );\n 3898|        send_frame(send, &frame);\n 3899|    }\n## daemon.rs\n 187|        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188|        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189|        let broker_handle: Option<Arc<Broker>> =\n 190|            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191|                Ok(broker) => {\n...\n 315|        // makes an update a *planned* restart on this same path: the supervisor's\n*316|        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n 317|        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n 318|        // onto the new binary with `start-reason=update`. The stop flag is held\n 319|        // for symmetry, never raised (the daemon runs until killed). The brain\n...\n 343|            if let Some(broker) = &broker_handle {\n*344|                broker.set_brain_restart(restart);\n 345|            }\n 346|        }\n 347|\n## msg.rs\n 231|/// which the caller surfaces — it never silently no-ops a swapped binary.\n*232|pub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n*233|/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n 234|/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235|/// acceptance, not completion. `honored` is false when the broker has no\n 236|/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237|/// reached a broker that cannot itself respawn.\n*238|pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239|\n 240|/// `spawn` payload — the program + args + initial geometry. The broker spawns\n 241|/// it under a PTY (no shell interpretation; argv is passed through).\n...\n 871|\n*872|/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n 873|/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874|/// cycle the child; false when the broker has none wired (a bare test broker),\n 875|/// so the caller can tell a real planned restart from a mere accept.\n 876|#[derive(Debug, Clone, Serialize, Deserialize)]\n*877|pub struct BrainRestarted {\n 878|    #[serde(default)]\n 879|    pub honored: bool,\n 880|}"}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":36,"fileCount":6,"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/msg.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":9},{"path":"crates/spt-daemon/src/brain.rs","count":7},{"path":"crates/spt-daemon/src/brainproc.rs","count":3},{"path":"crates/spt-daemon/src/broker.rs","count":9},{"path":"crates/spt-daemon/src/daemon.rs","count":3},{"path":"crates/spt-daemon/src/msg.rs","count":5}],"truncated":false,"displayContent":"# crates/spt-daemon/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 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 366│    brain\n*367│        .request_brain_restart()\n 368│        .map_err(|e| format!(\"brain-restart trigger: {e}\"))\n 369│}\n 370│\n    │...\n 792│    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n*793│    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n 794│    // supervisor — the test/no-daemon shape) accepts the verb but acks\n 795│    // `honored: false`. This is the broker half of the apply trigger that\n 796│    // replaces the in-process handoff.\n    │...\n 804│        assert!(\n*805│            !b.request_brain_restart().expect(\"verb round-trips\"),\n 806│            \"a broker with no supervisor wired must ack honored=false\"\n 807│        );\n 808│\n    │...\n 810│        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*811│        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 812│        let mut b2 = cold_connect_retry(&name);\n 813│        assert!(\n*814│            b2.request_brain_restart().expect(\"verb round-trips\"),\n 815│            \"a broker with a supervisor wired must ack honored=true\"\n 816│        );\n 817│        assert!(\n    │...\n 841│        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*842│        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 843│        assert!(\n 844│            refresh_brain(&name).expect(\"refresh round-trips\"),\n 845│            \"a wired supervisor must report honored=true\"\n## brain.rs\n   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   45│    ViewerEvictedEvent,\n  *46│    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n  *47│    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n   48│    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n   49│    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n   50│    KIND_NET_SENT,\n     │...\n 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  106│/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n *107│/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n  108│/// its next wait tick, kills the current brain child, and respawns it with\n  109│/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n  110│/// between the broker serve path and the supervisor — both run in the broker\n     │...\n 1092│/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n*1093│/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n 1094│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1095│pub fn spawn_brain_supervisor(\n 1096│    stop: Arc<AtomicBool>,\n     │...\n 1205│    /// The planned-restart path (D3-3, Q3): a `BrainRestart::request` (the\n*1206│    /// broker's `KIND_BRAIN_RESTART` handler) makes the supervisor cycle the\n 1207│    /// brain and stamp the next spawn `Update` — distinguishing an intentional\n 1208│    /// update cycle from a crash (V2, broker-authoritative). A long-lived child\n 1209│    /// is used so the restart, not an exit, is what ends the cycle.\n## broker.rs\n   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  *60│    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n   61│    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n   62│    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n   63│    KIND_NET_SENT,\n     │...\n 2541│    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542│    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543│    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n 2544│    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n 2545│    /// `Daemon::run` after it spawns the supervisor.\n     │...\n 2704│    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n*2705│    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n 2706│    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707│    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n 2708│    /// reports `honored: false` (no supervisor to respawn the child).\n 2709│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*2710│    pub fn set_brain_restart(&self, signal: Arc<crate::brainproc::BrainRestart>) -> bool {\n 2711│        self.brain_restart.set(signal).is_ok()\n 2712│    }\n 2713│\n     │...\n 3190│                }\n*3191│                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192│                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193│                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194│            }\n     │...\n 3893│        let frame = Envelope::new(\n*3894│            KIND_BRAIN_RESTARTED,\n*3895│            serde_json::to_value(BrainRestarted { honored })\n*3896│                .expect(\"BrainRestarted serializes\"),\n 3897│        );\n 3898│        send_frame(send, &frame);\n 3899│    }\n## daemon.rs\n 187│        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188│        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189│        let broker_handle: Option<Arc<Broker>> =\n 190│            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191│                Ok(broker) => {\n    │...\n 315│        // makes an update a *planned* restart on this same path: the supervisor's\n*316│        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n 317│        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n 318│        // onto the new binary with `start-reason=update`. The stop flag is held\n 319│        // for symmetry, never raised (the daemon runs until killed). The brain\n    │...\n 343│            if let Some(broker) = &broker_handle {\n*344│                broker.set_brain_restart(restart);\n 345│            }\n 346│        }\n 347│\n## msg.rs\n 231│/// which the caller surfaces — it never silently no-ops a swapped binary.\n*232│pub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n*233│/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n 234│/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235│/// acceptance, not completion. `honored` is false when the broker has no\n 236│/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237│/// reached a broker that cannot itself respawn.\n*238│pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239│\n 240│/// `spawn` payload — the program + args + initial geometry. The broker spawns\n 241│/// it under a PTY (no shell interpretation; argv is passed through).\n    │...\n 871│\n*872│/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n 873│/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874│/// cycle the child; false when the broker has none wired (a bare test broker),\n 875│/// so the caller can tell a real planned restart from a mere accept.\n 876│#[derive(Debug, Clone, Serialize, Deserialize)]\n*877│pub struct BrainRestarted {\n 878│    #[serde(default)]\n 879│    pub honored: bool,\n 880│}"},"isError":false,"timestamp":1784195766457}}
{"type":"message","id":"705a4f8a","parentId":"fa071f69","timestamp":"2026-07-16T09:56:06.586Z","message":{"role":"toolResult","toolCallId":"call_9jZ644qvMX1lVTBPGEtMBkBw|fc_0cc7a0c9a797f232016a58aab65e44819a977a22b1cb8bf773","toolName":"read","content":[{"type":"text","text":"    Subnet {\n…\n        action: Option<SubnetCmd>,\n    },\n    /// Task-oriented instructions for agents: `how-to <topic>`.\n    ///\n    /// The binary's own usage guidance, written for an agent to read and\n    /// follow. Bare `how-to` lists the topics.\n    #[command(name = \"how-to\")]\n    HowTo {\n        /// The topic to print (omit to list available topics).\n        topic: Option<String>,\n    },\n    /// Endpoint operations: list, lifecycle, fork, digest, access.\n    ///\n    /// The noun home for per-endpoint verbs (M8 decision 1). Bare\n    /// `spt endpoint` renders the merged listing — every member subnet's\n    /// endpoints grouped by subnet, this session's own endpoint pinned\n    /// distinctly at the top.\n    Endpoint {\n        #[command(subcommand)]\n        action: Option<EndpointCmd>,\n    },\n    /// Attach a local terminal to a broker-held endpoint PTY.\n    ///\n    /// Connects to an spt-hosted session and drives it as a terminal. Local is\n    /// the degenerate single-node case of the cross-node attach (one pump,\n    /// loopback peer). Detach with the **ctrl-b** prefix then `d`\n    /// (`ctrl-b ctrl-b` sends a literal ctrl-b); detaching leaves the session\n    /// running on the broker. `--view` watches read-only.\n    Rc {\n        /// The endpoint id whose broker-held session to attach.\n        id: String,\n        /// Read-only: render output, forward no input.\n        #[arg(long, conflicts_with = \"take\")]\n        view: bool,\n        /// Take control: kick the current controller (a loud notice to them) and\n        /// drive. Use on an endpoint another node controls.\n        #[arg(long)]\n        take: bool,\n    },\n    /// Consent grant store: gated capabilities held on this node.\n    ///\n    /// Default-deny (the access whitelist's opposite polarity). An\n    /// ungranted ask escalates interactively; `add` is the durable\n    /// allow-always answer.\n    Grant {\n        #[command(subcommand)]\n        action: GrantCmd,\n    },\n    /// Adapter registration: what this node can drive/launch.\n    ///\n    /// The node-local registered set (one command for harness and shell\n    /// adapters). Feeds creation-time adapter selection, shell discovery,\n    /// and the self-update ripple.\n    Adapter {\n        #[command(subcommand)]\n        action: AdapterCmd,\n    },\n    /// Shell instances: mint, list, drive, tear down owned surfaces.\n    ///\n    /// The driven surfaces this agent owns. `spawn` MINTS a new instance\n    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing\n    /// an existing offline instance back is `relink` / `persistent` /\n    /// wake.\n    Shell {\n        #[command(subcommand)]\n        action: ShellCmd,\n    },\n    /// Inspect and acknowledge notifications.\n    ///\n    /// Dismissal is the explicit ack — it latches and replicates\n    /// subnet-wide.\n    Notif {\n        #[command(subcommand)]\n        action: NotifCmd,\n    },\n    /// Self-update: bare `spt update` brings the whole node current.\n    ///\n    /// The bare form fetches + installs the latest core release, then updates\n    /// every release-shipped adapter — one command. The invoking session\n    /// survives it: installing cycles only the daemon's coordinator process,\n    /// never the hosted terminals. `apply` is the explicit ack named by the\n    /// update-consent notification; it re-verifies the staged release before\n    /// touching the live daemon.\n    #[command(args_conflicts_with_subcommands = true)]\n    Update {\n        #[command(subcommand)]\n        action: Option<UpdateCmd>,\n        /// Update the core binary only — skip the adapters leg of the bare\n        /// composite.\n        #[arg(long, short = 'c')]\n        core_only: bool,\n        /// The full-cycle form: fetch, update adapters, then finish by\n        /// restarting the daemon onto the new version (`update apply --finish`)\n        /// as the final step — so the whole node, coordinator and live agents,\n        /// runs the new version when it returns. The restart bounces hosted\n        /// sessions (they come back automatically).\n        #[arg(long)]\n        restart: bool,\n    },\n    /// The node-local docs: open them in your browser, or print their URL.\n    ///\n    /// Every release ships a version-matched docs bundle; the daemon serves\n    /// it on loopback. Bare `spt docs` opens the browser; `spt docs url`\n    /// prints the resolved URL for tools and agents.\n    Docs {\n        #[command(subcommand)]\n        action: Option<DocsCmd>,\n    },\n    /// Self-install this binary onto the node (the bootstrap path).\n    ///\n    /// Run it from a downloaded release binary: it places itself at the\n    /// canonical install dir, registers that dir on your user PATH, and\n    /// refuses a binary built for another platform. First-run identity and\n    /// daemon start happen on the first normal invocation, as always.\n    /// Non-interactive and idempotent — re-running is safe.\n    Install {\n        /// Install dir override (default: the spt home's bin dir).\n        #[arg(long)]\n        dir: Option<std::path::PathBuf>,\n        /// Skip user-PATH registration.\n        #[arg(long)]\n        no_path: bool,\n    },\n    /// Harness-contract inbound surface (hook entry points).\n    ///\n    /// The entry points a harness's hooks fire to keep spt-core's on-disk\n    /// state in sync.\n    Api(crate::api::ApiArgs),\n    /// The per-machine daemon: run, stop, or read node status.\n    ///\n    /// Bare `spt daemon` renders the node status view — daemon state,\n…\n        Cmd::Endpoint { action } => match action.unwrap_or(EndpointCmd::List {\n…\n        }) {\n…\n            EndpointCmd::Shutdown { id } => cmd_shutdown(id),\n            EndpointCmd::Stop { id } => cmd_stop(&id),\n            EndpointCmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),\n            EndpointCmd::Purge { id, yes, force } => cmd_endpoint_purge(&id, yes, force),\n            EndpointCmd::Digest {\n                id,\n                follow,\n                last,\n                after,\n            } => cmd_digest(&id, follow, json, last, after),\n            EndpointCmd::Access { action } => cmd_access(action, json),\n            EndpointCmd::Description { action } => {\n                cmd_description(action.unwrap_or(DescriptionCmd::Show { id: None }), json)\n            }\n            EndpointCmd::Role { id, overwrite } => cmd_role(id, overwrite, json),\n            EndpointCmd::Run {\n                adapter,\n                id,\n                // `--create` is the default-fresh made explicit (REQ-HOST-RUN-1\n                // flag symmetry so a shortcut can bake create-vs-resume); at\n                // dispatch it only matters as the ABSENCE of --resume, so it is\n                // not threaded further. clap already conflicts it with --resume.\n                create: _,\n                resume,\n                start,\n                attach,\n                view,\n                subnet,\n                save,\n            } => {\n                // Route the 4 (adapter?,id?) quadrants. A lone `--id` naming an\n                // EXISTING perch reuses its recorded adapter and runs\n                // non-interactively (D-1) rather than falling to the create-new\n                // picker; the perch's info.adapter is injected so the router is pure.\n                // [impl->REQ-RUN-ID-REUSES-ADAPTER]\n                let target = resolve_run_target(adapter, id, |id| {\n                    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n                    spt_store::info::read_info(&perch).and_then(|i| i.adapter)\n                });\n                match target {\n                    // Direct CLI: no picker-threaded cwd — cmd_endpoint_run\n                    // resolves it (resume info.cwd → current_dir).\n                    RunTarget::Direct { adapter, id } => cmd_endpoint_run(\n                        &adapter, &id, resume, None, start, attach, view, subnet.as_deref(), save,\n                    ),\n                    // Bare (or partial) → the interactive picker (REQ-RUN-PICKER).\n                    // A lone --adapter pre-selects it; a lone --id with no existing\n                    // perch pre-fills the create-new id.\n                    RunTarget::Picker { adapter, id } => crate::picker::run(adapter, id),\n                }\n            }\n        },\n        Cmd::Rc { id, view, take } => {\n            let intent = rc_intent(view, take);\n            match crate::rc::run_attach(&id, intent) {\n                Ok(()) => 0,\n                Err(e) => {\n                    eprintln!(\"RC_FAIL:{id}: {e}\");\n                    1\n                }\n            }\n        }\n        Cmd::Grant { action } => cmd_grant(action, json),\n        Cmd::Adapter { action } => cmd_adapter(action, json),\n        Cmd::Shell { action } => cmd_shell(action, json),\n        Cmd::Notif { action } => match action {\n            NotifCmd::List { subnet } => cmd_notif_list(subnet, json),\n            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),\n        },\n        Cmd::Update {\n            action,\n            core_only,\n            restart,\n        } => match action {\n            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n            Some(UpdateCmd::Fetch {\n                channel,\n                tag,\n                apply,\n            }) => cmd_update_fetch(channel, tag, apply),\n            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n            Some(UpdateCmd::Adapters { names }) => {\n                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n            }\n            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n            None => cmd_update_composite(core_only, restart),\n        },\n        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n        Cmd::Install { dir, no_path } => cmd_install(dir, no_path),\n        // [impl->REQ-DOCS-LOCAL-SERVER]\n        Cmd::Docs { action } => cmd_docs(action),\n        Cmd::Api(args) => crate::api::run(args),\n        Cmd::Spt => {\n            println!(\"Sentience Pocket Transacter\");\n            0\n        }\n        // [impl->REQ-SPOOL-TAKE-AUDIT] debug read of a perch's spool taker-audit.\n        Cmd::SpoolAudit { id, json } => cmd_spool_audit(&id, json),\n        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.\n        // [impl->REQ-CLI-2]\n…\nfn cmd_daemon_start() -> i32 {\n    match spt_daemon::start_daemon() {\n…\n        Ok(spt_daemon::StartOutcome::ServiceStarted(label)) => {\n…\n        }\n        Ok(spt_daemon::StartOutcome::Spawned) => {\n            eprintln!(\"DAEMON_STARTED\");\n            0\n        }\n        Err(e) => {\n            eprintln!(\"DAEMON_START_FAIL: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon stop` — graceful, service-aware shutdown (REQ-DAEMON-6, M8\n/// decision 5 / REQ-CLI-2): a managed service is stopped through its manager\n/// (so it does not auto-restart-fight); a manual daemon acks the seed-control\n/// stop then exits. Idempotent UX: not-running is already-stopped (exit 0).\n/// The live-session stop guard (REQ-DAEMON-STOP-LIVE-SESSION-WARN): pure — a daemon\n/// hosting live sessions REFUSES to stop (naming them) unless `--force`, so a stop\n/// never silently kills hosted endpoints out from under an operator. Empty set (no\n/// hosted sessions) or `--force` ⇒ proceed. `Err` carries the operator warning.\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn stop_live_session_guard(live_sessions: &[String], force: bool) -> Result<(), String> {\n    if live_sessions.is_empty() || force {\n        return Ok(());\n    }\n    Err(format!(\n        \"the daemon is hosting {} live session(s) that stopping would kill: {}. \\\n         Re-run with `spt daemon stop --force` to stop anyway — they come back on the \\\n         next `spt daemon start` — or leave the daemon running.\",\n        live_sessions.len(),\n        live_sessions.join(\", \")\n    ))\n}\n\n/// The endpoint ids the running daemon currently hosts a broker PTY session for —\n/// the stop-guard's \"would be killed\" set. Best-effort: a down/unreachable daemon\n/// yields none (the guard is then a no-op and the normal stop path reports\n/// already-stopped). // [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn live_hosted_session_ids() -> Vec<String> {\n    if !spt_daemon::is_running() {\n        return Vec::new();\n    }\n    match spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms()) {\n        Ok(mut b) => b\n            .sessions()\n            .map(|r| {\n                r.sessions\n                    .into_iter()\n                    .map(|s| s.endpoint)\n                    .filter(|e| !e.is_empty())\n                    .collect()\n            })\n            .unwrap_or_default(),\n        Err(_) => Vec::new(),\n    }\n}\n\n// [impl->REQ-DAEMON-6]\n// [impl->REQ-CLI-2]\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn cmd_daemon_stop(force: bool) -> i32 {\n    // Guard: refuse + name the sessions if stopping would kill live hosted endpoints,\n    // unless --force. Runs BEFORE the stop so nothing is torn down on a refusal.\n    let live = live_hosted_session_ids();\n    if let Err(msg) = stop_live_session_guard(&live, force) {\n        eprintln!(\"DAEMON_STOP_REFUSED: {msg}\");\n        return 3;\n    }\n    match spt_daemon::stop_daemon() {\n        Ok(spt_daemon::StopOutcome::NotRunning) => {\n            eprintln!(\"DAEMON_NOT_RUNNING (already stopped)\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::ServiceStopped(label)) => {\n            eprintln!(\"DAEMON_STOPPED (via {label})\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::IpcStopped) => {\n            eprintln!(\"DAEMON_STOPPED\");\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…\nfn report_apply_outcome(outcome: spt_daemon::ApplyStagedOutcome) -> i32 {\n…\n    match outcome {\n…\n        O::Quarantined { version } => {\n…\n            1\n        }\n        O::RefusedClass(class) => {\n            eprintln!(\"UPDATE_APPLY_REFUSED:{class:?} (full-swap orchestration is not M5 scope)\");\n            1\n        }\n        O::Rejected(reason) => {\n            eprintln!(\"UPDATE_APPLY_REJECTED:{reason}\");\n            1\n        }\n    }\n}\n\n/// `spt update apply [--finish]` — the explicit ack named by the update-consent\n/// notif. NO LONGER boots the daemon first (the REQ-UPDATE-ONE-SHOT-FINISH wart):\n/// a stopped box used to `ensure_daemon_announced` the OLD broker up just to hand\n/// off to it, guaranteeing a mixed old-broker/new-brain pair + a manual bounce.\n/// Now: daemon up ⇒ in-place brain handoff; daemon down ⇒ a daemonless swap (next\n/// start runs it); `--finish` ⇒ swap then restart the daemon onto the new bytes.\n// [impl->REQ-UPD-4]\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\nfn cmd_update_apply(finish: bool) -> i32 {\n    let exe = match std::env::current_exe() {\n        Ok(p) => p,\n        Err(e) => {\n            eprintln!(\"CURRENT_EXE_FAIL: {e}\");\n            return 1;\n        }\n    };\n    let cache = spt_daemon::ReleaseCache::open(&perch::spt_home().join(\"releases\"));\n    let keys = perch::identity_dir().join(\"release-keys.json\");\n\n    if finish {\n        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n        if code == 0 {\n            land_staged_docs(&cache);\n        }\n        return code;\n    }\n\n    // Daemon up ⇒ hand off to the running broker in place (the brain cycles onto\n    // new bytes immediately; broker survives, REQ-UPD-3). Daemon down ⇒ swap\n    // daemonless and never boot the old broker — the next start runs the new\n    // bytes (REQ-UPDATE-ONE-SHOT-FINISH).\n    let outcome = if spt_daemon::is_running() {\n        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\n    } else {\n        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\n    };\n    match outcome {\n        Ok(o) => {\n            let code = report_apply_outcome(o);\n            // Docs land only AFTER a successful binary apply, and their\n            // failure can never change its outcome (ADR-0036 §4).\n            // [impl->REQ-DOCS-RELEASE-ASSET]\n            if code == 0 {\n                land_staged_docs(&cache);\n            }\n            code\n        }\n        Err(e) => {\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\n            1\n        }\n    }\n}\n\n/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\n/// REQ-UPDATE-RESTART-SAFE-SWAP).\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\nenum UpdateLeg {\n    /// `update fetch --apply` — stage + install the core in one step (the\n    /// brain-only cycle: the invoking process survives by construction).\n    FetchApply,\n    /// `update fetch` — stage only; the swap waits for the Finish leg.\n    Fetch,\n    /// `update adapters` — sweep every gh_release adapter.\n    Adapters,\n    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\n    /// spt-hosted session the invoker's PTY dies here, so this leg is always\n    /// planned LAST (everything else has completed by then).\n    Finish,\n}\n\n/// Plan the composite's leg sequence — pure, unit-gated. The invariants:\n/// core before adapters (core-first doctrine); `--core-only` drops the\n/// adapters leg; `--restart` replaces the in-place apply with stage-now\n/// (`Fetch`) + `Finish` LAST (lethal-leg-last, operator-ruled: the whole-daemon\n/// restart must be the final act so every other leg completes from ANY\n/// invoking context, including a hosted session whose PTY the finish kills).\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\nfn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\n    let mut legs = Vec::new();\n    legs.push(if restart {\n        UpdateLeg::Fetch\n    } else {\n        UpdateLeg::FetchApply\n    });\n    if !core_only {\n        legs.push(UpdateLeg::Adapters);\n    }\n    if restart {\n        legs.push(UpdateLeg::Finish);\n    }\n    legs\n}\n\n/// Bare `spt update` — the composite front door (REQ-UPDATE-DEFAULT-COMPOSITE):\n/// core first (`fetch --apply`; an already-current core no-ops and the run\n/// becomes adapters-only), then `update adapters`. The invoker survives the\n/// core leg by construction — apply cycles the BRAIN only, the broker and every\n/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n///\n/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\n/// ABORTS the remaining legs — updating adapters against a channel that just\n/// refused is compounding, and a `--restart` finish after a failed fetch would\n/// bounce the daemon for nothing. An ADAPTERS-leg failure is isolated (that is\n/// its contract): loud, nonzero at exit, but the composite proceeds — under\n/// `--restart` the finish still completes the core cycle. The accepted\n/// asymmetry (new adapters briefly on old-activated core if the finish itself\n/// fails) is loud and operator-attended by nature of the flag.\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\nfn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n    let mut worst = 0;\n    for leg in plan_update_legs(core_only, restart) {\n        let code = match leg {\n            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\n            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\n            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\n            UpdateLeg::Finish => cmd_update_apply(true),\n        };\n        if code != 0 {\n            if composite_abort_on_failure(leg) {\n                eprintln!(\n                    \"UPDATE_COMPOSITE_ABORTED: the core update failed — the remaining \\\n                     leg(s) were not run\"\n                );\n                return code;\n            }\n            worst = code;\n        }\n    }\n    worst\n}\n\n/// Whether a failed composite leg aborts the remaining legs — pure, unit-gated.\n/// Only the CORE legs abort (a refused channel poisons the adapters leg, and a\n/// finish after a failed fetch is a pointless daemon bounce); an adapters-leg\n/// failure is isolated by contract and the composite proceeds.\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\nfn composite_abort_on_failure(leg: UpdateLeg) -> bool {\n    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\n}\n\n/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\n/// version-matched to the just-applied release (ADR-0036 §4). Extraction is a\n/// bounded system `tar` (the REQ-INSTALL-9 archive precedent) into `docs.new`,\n/// then a swap so a torn extract never leaves a half-tree at `docs`. EVERY\n/// failure degrades to the machine-greppable `UPDATE_DOCS_SKIPPED` on stderr\n/// and leaves the staged bundle in place for the next fetch/apply to retry;\n/// the binary-apply outcome is decided before this runs and never changes.\n// [impl->REQ-DOCS-RELEASE-ASSET]\nfn land_staged_docs(cache: &spt_daemon::ReleaseCache) {\n    let Some(bundle) = cache.staged_docs() else {\n        return; // docs-less release or docs fetch skipped — nothing to land\n    };\n    // Re-verify against the SIGNED staged metadata before touching disk (the\n    // cache is trusted-on-stage, but re-hashing 1 file is cheap insurance).\n    let verified = match cache.staged_update() {\n        Some(spt_daemon::StagedUpdate::Set(signed)) => {\n            serde_json::from_str::<spt_daemon::UpdateSetMetadata>(&signed.metadata_json)\n                .ok()\n                .map(|meta| spt_daemon::verify_update_set_docs(&meta, &bundle).is_ok())\n        }\n…\n    };\n…\n    // Landed — the staged bundle has served its purpose.\n    cache.clear_staged_docs();\n    println!(\"UPDATE_DOCS_LANDED: {}\", docs_dir.display());\n}\n\n/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\n/// coordinator running new-agent code), then RESTART the daemon onto them so the\n/// whole node runs the new version in one command. The restart rides\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\n/// online hosted endpoints, so the cycle is not a massacre.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\nfn cmd_update_apply_finish(\n    cache: &spt_daemon::ReleaseCache,\n    keys: &std::path::Path,\n    exe: &std::path::Path,\n) -> i32 {\n    use spt_daemon::ApplyStagedOutcome as O;\n\n    // 1. Swap the bytes with NO broker touched. A daemonless swap can never leave\n    //    a running old broker paired with new brain bytes.\n    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\n        Ok(O::NoUpdate) => {\n            println!(\"NO_UPDATE\");\n            return 0;\n        }\n        // The proceed set — bytes are (or already were) on disk; go restart onto them.\n        Ok(o @ O::AppliedDaemonless { .. })\n        | Ok(o @ O::AppliedPending { .. })\n        | Ok(o @ O::AlreadyApplied { .. }) => o,\n        // The fail-closed refusals — nothing swapped, do NOT restart.\n        Ok(refused) => return report_apply_outcome(refused),\n        Err(e) => {\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\n            return 1;\n        }\n    };\n\n    // 2. Restart the daemon onto the new bytes. Announce the sessions the cycle\n    //    will bounce (they come back on start), stop if up, WAIT for it to be\n    //    actually down (else `start` would see it \"already running\" and skip the\n    //    swap onto new bytes), then start.\n    if spt_daemon::is_running() {\n        let hosted = live_hosted_session_ids();\n        if !hosted.is_empty() {\n            eprintln!(\n                \"Cycling {} hosted session(s) onto the new version (they come back automatically): {}\",\n                hosted.len(),\n                hosted.join(\", \")\n            );\n        }\n        if let Err(e) = spt_daemon::stop_daemon() {\n            eprintln!(\n                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n            );\n            return 1;\n        }\n        let down_by = std::time::Instant::now() + std::time::Duration::from_secs(10);\n        while spt_daemon::is_running() && std::time::Instant::now() < down_by {\n            std::thread::sleep(std::time::Duration::from_millis(50));\n        }\n        if spt_daemon::is_running() {\n            eprintln!(\n                \"DAEMON_STOP_TIMEOUT: the daemon did not stop within 10s — the new version is on \\\n                 disk; run `spt daemon start` once it stops to finish.\"\n            );\n            return 1;\n        }\n    }\n    match spt_daemon::start_daemon() {\n        Ok(_) => {\n            let (version, product_version, already) = match applied {\n                O::AppliedDaemonless {\n                    version,\n                    product_version,\n                }\n                | O::AppliedPending {\n…\n                } => (version, product_version, false),\n…\n            };\n…\n        }\n…\n    }\n}"}],"details":{"displayContent":{"text":"    Subnet {\n…\n        action: Option<SubnetCmd>,\n    },\n    /// Task-oriented instructions for agents: `how-to <topic>`.\n    ///\n    /// The binary's own usage guidance, written for an agent to read and\n    /// follow. Bare `how-to` lists the topics.\n    #[command(name = \"how-to\")]\n    HowTo {\n        /// The topic to print (omit to list available topics).\n        topic: Option<String>,\n    },\n    /// Endpoint operations: list, lifecycle, fork, digest, access.\n    ///\n    /// The noun home for per-endpoint verbs (M8 decision 1). Bare\n    /// `spt endpoint` renders the merged listing — every member subnet's\n    /// endpoints grouped by subnet, this session's own endpoint pinned\n    /// distinctly at the top.\n    Endpoint {\n        #[command(subcommand)]\n        action: Option<EndpointCmd>,\n    },\n    /// Attach a local terminal to a broker-held endpoint PTY.\n    ///\n    /// Connects to an spt-hosted session and drives it as a terminal. Local is\n    /// the degenerate single-node case of the cross-node attach (one pump,\n    /// loopback peer). Detach with the **ctrl-b** prefix then `d`\n    /// (`ctrl-b ctrl-b` sends a literal ctrl-b); detaching leaves the session\n    /// running on the broker. `--view` watches read-only.\n    Rc {\n        /// The endpoint id whose broker-held session to attach.\n        id: String,\n        /// Read-only: render output, forward no input.\n        #[arg(long, conflicts_with = \"take\")]\n        view: bool,\n        /// Take control: kick the current controller (a loud notice to them) and\n        /// drive. Use on an endpoint another node controls.\n        #[arg(long)]\n        take: bool,\n    },\n    /// Consent grant store: gated capabilities held on this node.\n    ///\n    /// Default-deny (the access whitelist's opposite polarity). An\n    /// ungranted ask escalates interactively; `add` is the durable\n    /// allow-always answer.\n    Grant {\n        #[command(subcommand)]\n        action: GrantCmd,\n    },\n    /// Adapter registration: what this node can drive/launch.\n    ///\n    /// The node-local registered set (one command for harness and shell\n    /// adapters). Feeds creation-time adapter selection, shell discovery,\n    /// and the self-update ripple.\n    Adapter {\n        #[command(subcommand)]\n        action: AdapterCmd,\n    },\n    /// Shell instances: mint, list, drive, tear down owned surfaces.\n    ///\n    /// The driven surfaces this agent owns. `spawn` MINTS a new instance\n    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing\n    /// an existing offline instance back is `relink` / `persistent` /\n    /// wake.\n    Shell {\n        #[command(subcommand)]\n        action: ShellCmd,\n    },\n    /// Inspect and acknowledge notifications.\n    ///\n    /// Dismissal is the explicit ack — it latches and replicates\n    /// subnet-wide.\n    Notif {\n        #[command(subcommand)]\n        action: NotifCmd,\n    },\n    /// Self-update: bare `spt update` brings the whole node current.\n    ///\n    /// The bare form fetches + installs the latest core release, then updates\n    /// every release-shipped adapter — one command. The invoking session\n    /// survives it: installing cycles only the daemon's coordinator process,\n    /// never the hosted terminals. `apply` is the explicit ack named by the\n    /// update-consent notification; it re-verifies the staged release before\n    /// touching the live daemon.\n    #[command(args_conflicts_with_subcommands = true)]\n    Update {\n        #[command(subcommand)]\n        action: Option<UpdateCmd>,\n        /// Update the core binary only — skip the adapters leg of the bare\n        /// composite.\n        #[arg(long, short = 'c')]\n        core_only: bool,\n        /// The full-cycle form: fetch, update adapters, then finish by\n        /// restarting the daemon onto the new version (`update apply --finish`)\n        /// as the final step — so the whole node, coordinator and live agents,\n        /// runs the new version when it returns. The restart bounces hosted\n        /// sessions (they come back automatically).\n        #[arg(long)]\n        restart: bool,\n    },\n    /// The node-local docs: open them in your browser, or print their URL.\n    ///\n    /// Every release ships a version-matched docs bundle; the daemon serves\n    /// it on loopback. Bare `spt docs` opens the browser; `spt docs url`\n    /// prints the resolved URL for tools and agents.\n    Docs {\n        #[command(subcommand)]\n        action: Option<DocsCmd>,\n    },\n    /// Self-install this binary onto the node (the bootstrap path).\n    ///\n    /// Run it from a downloaded release binary: it places itself at the\n    /// canonical install dir, registers that dir on your user PATH, and\n    /// refuses a binary built for another platform. First-run identity and\n    /// daemon start happen on the first normal invocation, as always.\n    /// Non-interactive and idempotent — re-running is safe.\n    Install {\n        /// Install dir override (default: the spt home's bin dir).\n        #[arg(long)]\n        dir: Option<std::path::PathBuf>,\n        /// Skip user-PATH registration.\n        #[arg(long)]\n        no_path: bool,\n    },\n    /// Harness-contract inbound surface (hook entry points).\n    ///\n    /// The entry points a harness's hooks fire to keep spt-core's on-disk\n    /// state in sync.\n    Api(crate::api::ApiArgs),\n    /// The per-machine daemon: run, stop, or read node status.\n    ///\n    /// Bare `spt daemon` renders the node status view — daemon state,\n…\n        Cmd::Endpoint { action } => match action.unwrap_or(EndpointCmd::List {\n…\n        }) {\n…\n            EndpointCmd::Shutdown { id } => cmd_shutdown(id),\n            EndpointCmd::Stop { id } => cmd_stop(&id),\n            EndpointCmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),\n            EndpointCmd::Purge { id, yes, force } => cmd_endpoint_purge(&id, yes, force),\n            EndpointCmd::Digest {\n                id,\n                follow,\n                last,\n                after,\n            } => cmd_digest(&id, follow, json, last, after),\n            EndpointCmd::Access { action } => cmd_access(action, json),\n            EndpointCmd::Description { action } => {\n                cmd_description(action.unwrap_or(DescriptionCmd::Show { id: None }), json)\n            }\n            EndpointCmd::Role { id, overwrite } => cmd_role(id, overwrite, json),\n            EndpointCmd::Run {\n                adapter,\n                id,\n                // `--create` is the default-fresh made explicit (REQ-HOST-RUN-1\n                // flag symmetry so a shortcut can bake create-vs-resume); at\n                // dispatch it only matters as the ABSENCE of --resume, so it is\n                // not threaded further. clap already conflicts it with --resume.\n                create: _,\n                resume,\n                start,\n                attach,\n                view,\n                subnet,\n                save,\n            } => {\n                // Route the 4 (adapter?,id?) quadrants. A lone `--id` naming an\n                // EXISTING perch reuses its recorded adapter and runs\n                // non-interactively (D-1) rather than falling to the create-new\n                // picker; the perch's info.adapter is injected so the router is pure.\n                // [impl->REQ-RUN-ID-REUSES-ADAPTER]\n                let target = resolve_run_target(adapter, id, |id| {\n                    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n                    spt_store::info::read_info(&perch).and_then(|i| i.adapter)\n                });\n                match target {\n                    // Direct CLI: no picker-threaded cwd — cmd_endpoint_run\n                    // resolves it (resume info.cwd → current_dir).\n                    RunTarget::Direct { adapter, id } => cmd_endpoint_run(\n                        &adapter, &id, resume, None, start, attach, view, subnet.as_deref(), save,\n                    ),\n                    // Bare (or partial) → the interactive picker (REQ-RUN-PICKER).\n                    // A lone --adapter pre-selects it; a lone --id with no existing\n                    // perch pre-fills the create-new id.\n                    RunTarget::Picker { adapter, id } => crate::picker::run(adapter, id),\n                }\n            }\n        },\n        Cmd::Rc { id, view, take } => {\n            let intent = rc_intent(view, take);\n            match crate::rc::run_attach(&id, intent) {\n                Ok(()) => 0,\n                Err(e) => {\n                    eprintln!(\"RC_FAIL:{id}: {e}\");\n                    1\n                }\n            }\n        }\n        Cmd::Grant { action } => cmd_grant(action, json),\n        Cmd::Adapter { action } => cmd_adapter(action, json),\n        Cmd::Shell { action } => cmd_shell(action, json),\n        Cmd::Notif { action } => match action {\n            NotifCmd::List { subnet } => cmd_notif_list(subnet, json),\n            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),\n        },\n        Cmd::Update {\n            action,\n            core_only,\n            restart,\n        } => match action {\n            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n            Some(UpdateCmd::Fetch {\n                channel,\n                tag,\n                apply,\n            }) => cmd_update_fetch(channel, tag, apply),\n            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n            Some(UpdateCmd::Adapters { names }) => {\n                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n            }\n            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n            None => cmd_update_composite(core_only, restart),\n        },\n        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n        Cmd::Install { dir, no_path } => cmd_install(dir, no_path),\n        // [impl->REQ-DOCS-LOCAL-SERVER]\n        Cmd::Docs { action } => cmd_docs(action),\n        Cmd::Api(args) => crate::api::run(args),\n        Cmd::Spt => {\n            println!(\"Sentience Pocket Transacter\");\n            0\n        }\n        // [impl->REQ-SPOOL-TAKE-AUDIT] debug read of a perch's spool taker-audit.\n        Cmd::SpoolAudit { id, json } => cmd_spool_audit(&id, json),\n        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.\n        // [impl->REQ-CLI-2]\n…\nfn cmd_daemon_start() -> i32 {\n    match spt_daemon::start_daemon() {\n…\n        Ok(spt_daemon::StartOutcome::ServiceStarted(label)) => {\n…\n        }\n        Ok(spt_daemon::StartOutcome::Spawned) => {\n            eprintln!(\"DAEMON_STARTED\");\n            0\n        }\n        Err(e) => {\n            eprintln!(\"DAEMON_START_FAIL: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon stop` — graceful, service-aware shutdown (REQ-DAEMON-6, M8\n/// decision 5 / REQ-CLI-2): a managed service is stopped through its manager\n/// (so it does not auto-restart-fight); a manual daemon acks the seed-control\n/// stop then exits. Idempotent UX: not-running is already-stopped (exit 0).\n/// The live-session stop guard (REQ-DAEMON-STOP-LIVE-SESSION-WARN): pure — a daemon\n/// hosting live sessions REFUSES to stop (naming them) unless `--force`, so a stop\n/// never silently kills hosted endpoints out from under an operator. Empty set (no\n/// hosted sessions) or `--force` ⇒ proceed. `Err` carries the operator warning.\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn stop_live_session_guard(live_sessions: &[String], force: bool) -> Result<(), String> {\n    if live_sessions.is_empty() || force {\n        return Ok(());\n    }\n    Err(format!(\n        \"the daemon is hosting {} live session(s) that stopping would kill: {}. \\\n         Re-run with `spt daemon stop --force` to stop anyway — they come back on the \\\n         next `spt daemon start` — or leave the daemon running.\",\n        live_sessions.len(),\n        live_sessions.join(\", \")\n    ))\n}\n\n/// The endpoint ids the running daemon currently hosts a broker PTY session for —\n/// the stop-guard's \"would be killed\" set. Best-effort: a down/unreachable daemon\n/// yields none (the guard is then a no-op and the normal stop path reports\n/// already-stopped). // [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn live_hosted_session_ids() -> Vec<String> {\n    if !spt_daemon::is_running() {\n        return Vec::new();\n    }\n    match spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms()) {\n        Ok(mut b) => b\n            .sessions()\n            .map(|r| {\n                r.sessions\n                    .into_iter()\n                    .map(|s| s.endpoint)\n                    .filter(|e| !e.is_empty())\n                    .collect()\n            })\n            .unwrap_or_default(),\n        Err(_) => Vec::new(),\n    }\n}\n\n// [impl->REQ-DAEMON-6]\n// [impl->REQ-CLI-2]\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn cmd_daemon_stop(force: bool) -> i32 {\n    // Guard: refuse + name the sessions if stopping would kill live hosted endpoints,\n    // unless --force. Runs BEFORE the stop so nothing is torn down on a refusal.\n    let live = live_hosted_session_ids();\n    if let Err(msg) = stop_live_session_guard(&live, force) {\n        eprintln!(\"DAEMON_STOP_REFUSED: {msg}\");\n        return 3;\n    }\n    match spt_daemon::stop_daemon() {\n        Ok(spt_daemon::StopOutcome::NotRunning) => {\n            eprintln!(\"DAEMON_NOT_RUNNING (already stopped)\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::ServiceStopped(label)) => {\n            eprintln!(\"DAEMON_STOPPED (via {label})\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::IpcStopped) => {\n            eprintln!(\"DAEMON_STOPPED\");\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…\nfn report_apply_outcome(outcome: spt_daemon::ApplyStagedOutcome) -> i32 {\n…\n    match outcome {\n…\n        O::Quarantined { version } => {\n…\n            1\n        }\n        O::RefusedClass(class) => {\n            eprintln!(\"UPDATE_APPLY_REFUSED:{class:?} (full-swap orchestration is not M5 scope)\");\n            1\n        }\n        O::Rejected(reason) => {\n            eprintln!(\"UPDATE_APPLY_REJECTED:{reason}\");\n            1\n        }\n    }\n}\n\n/// `spt update apply [--finish]` — the explicit ack named by the update-consent\n/// notif. NO LONGER boots the daemon first (the REQ-UPDATE-ONE-SHOT-FINISH wart):\n/// a stopped box used to `ensure_daemon_announced` the OLD broker up just to hand\n/// off to it, guaranteeing a mixed old-broker/new-brain pair + a manual bounce.\n/// Now: daemon up ⇒ in-place brain handoff; daemon down ⇒ a daemonless swap (next\n/// start runs it); `--finish` ⇒ swap then restart the daemon onto the new bytes.\n// [impl->REQ-UPD-4]\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\nfn cmd_update_apply(finish: bool) -> i32 {\n    let exe = match std::env::current_exe() {\n        Ok(p) => p,\n        Err(e) => {\n            eprintln!(\"CURRENT_EXE_FAIL: {e}\");\n            return 1;\n        }\n    };\n    let cache = spt_daemon::ReleaseCache::open(&perch::spt_home().join(\"releases\"));\n    let keys = perch::identity_dir().join(\"release-keys.json\");\n\n    if finish {\n        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n        if code == 0 {\n            land_staged_docs(&cache);\n        }\n        return code;\n    }\n\n    // Daemon up ⇒ hand off to the running broker in place (the brain cycles onto\n    // new bytes immediately; broker survives, REQ-UPD-3). Daemon down ⇒ swap\n    // daemonless and never boot the old broker — the next start runs the new\n    // bytes (REQ-UPDATE-ONE-SHOT-FINISH).\n    let outcome = if spt_daemon::is_running() {\n        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\n    } else {\n        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\n    };\n    match outcome {\n        Ok(o) => {\n            let code = report_apply_outcome(o);\n            // Docs land only AFTER a successful binary apply, and their\n            // failure can never change its outcome (ADR-0036 §4).\n            // [impl->REQ-DOCS-RELEASE-ASSET]\n            if code == 0 {\n                land_staged_docs(&cache);\n            }\n            code\n        }\n        Err(e) => {\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\n            1\n        }\n    }\n}\n\n/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\n/// REQ-UPDATE-RESTART-SAFE-SWAP).\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\nenum UpdateLeg {\n    /// `update fetch --apply` — stage + install the core in one step (the\n    /// brain-only cycle: the invoking process survives by construction).\n    FetchApply,\n    /// `update fetch` — stage only; the swap waits for the Finish leg.\n    Fetch,\n    /// `update adapters` — sweep every gh_release adapter.\n    Adapters,\n    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\n    /// spt-hosted session the invoker's PTY dies here, so this leg is always\n    /// planned LAST (everything else has completed by then).\n    Finish,\n}\n\n/// Plan the composite's leg sequence — pure, unit-gated. The invariants:\n/// core before adapters (core-first doctrine); `--core-only` drops the\n/// adapters leg; `--restart` replaces the in-place apply with stage-now\n/// (`Fetch`) + `Finish` LAST (lethal-leg-last, operator-ruled: the whole-daemon\n/// restart must be the final act so every other leg completes from ANY\n/// invoking context, including a hosted session whose PTY the finish kills).\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\nfn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\n    let mut legs = Vec::new();\n    legs.push(if restart {\n        UpdateLeg::Fetch\n    } else {\n        UpdateLeg::FetchApply\n    });\n    if !core_only {\n        legs.push(UpdateLeg::Adapters);\n    }\n    if restart {\n        legs.push(UpdateLeg::Finish);\n    }\n    legs\n}\n\n/// Bare `spt update` — the composite front door (REQ-UPDATE-DEFAULT-COMPOSITE):\n/// core first (`fetch --apply`; an already-current core no-ops and the run\n/// becomes adapters-only), then `update adapters`. The invoker survives the\n/// core leg by construction — apply cycles the BRAIN only, the broker and every\n/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n///\n/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\n/// ABORTS the remaining legs — updating adapters against a channel that just\n/// refused is compounding, and a `--restart` finish after a failed fetch would\n/// bounce the daemon for nothing. An ADAPTERS-leg failure is isolated (that is\n/// its contract): loud, nonzero at exit, but the composite proceeds — under\n/// `--restart` the finish still completes the core cycle. The accepted\n/// asymmetry (new adapters briefly on old-activated core if the finish itself\n/// fails) is loud and operator-attended by nature of the flag.\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\nfn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n    let mut worst = 0;\n    for leg in plan_update_legs(core_only, restart) {\n        let code = match leg {\n            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\n            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\n            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\n            UpdateLeg::Finish => cmd_update_apply(true),\n        };\n        if code != 0 {\n            if composite_abort_on_failure(leg) {\n                eprintln!(\n                    \"UPDATE_COMPOSITE_ABORTED: the core update failed — the remaining \\\n                     leg(s) were not run\"\n                );\n                return code;\n            }\n            worst = code;\n        }\n    }\n    worst\n}\n\n/// Whether a failed composite leg aborts the remaining legs — pure, unit-gated.\n/// Only the CORE legs abort (a refused channel poisons the adapters leg, and a\n/// finish after a failed fetch is a pointless daemon bounce); an adapters-leg\n/// failure is isolated by contract and the composite proceeds.\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\nfn composite_abort_on_failure(leg: UpdateLeg) -> bool {\n    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\n}\n\n/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\n/// version-matched to the just-applied release (ADR-0036 §4). Extraction is a\n/// bounded system `tar` (the REQ-INSTALL-9 archive precedent) into `docs.new`,\n/// then a swap so a torn extract never leaves a half-tree at `docs`. EVERY\n/// failure degrades to the machine-greppable `UPDATE_DOCS_SKIPPED` on stderr\n/// and leaves the staged bundle in place for the next fetch/apply to retry;\n/// the binary-apply outcome is decided before this runs and never changes.\n// [impl->REQ-DOCS-RELEASE-ASSET]\nfn land_staged_docs(cache: &spt_daemon::ReleaseCache) {\n    let Some(bundle) = cache.staged_docs() else {\n        return; // docs-less release or docs fetch skipped — nothing to land\n    };\n    // Re-verify against the SIGNED staged metadata before touching disk (the\n    // cache is trusted-on-stage, but re-hashing 1 file is cheap insurance).\n    let verified = match cache.staged_update() {\n        Some(spt_daemon::StagedUpdate::Set(signed)) => {\n            serde_json::from_str::<spt_daemon::UpdateSetMetadata>(&signed.metadata_json)\n                .ok()\n                .map(|meta| spt_daemon::verify_update_set_docs(&meta, &bundle).is_ok())\n        }\n…\n    };\n…\n    // Landed — the staged bundle has served its purpose.\n    cache.clear_staged_docs();\n    println!(\"UPDATE_DOCS_LANDED: {}\", docs_dir.display());\n}\n\n/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\n/// coordinator running new-agent code), then RESTART the daemon onto them so the\n/// whole node runs the new version in one command. The restart rides\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\n/// online hosted endpoints, so the cycle is not a massacre.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\nfn cmd_update_apply_finish(\n    cache: &spt_daemon::ReleaseCache,\n    keys: &std::path::Path,\n    exe: &std::path::Path,\n) -> i32 {\n    use spt_daemon::ApplyStagedOutcome as O;\n\n    // 1. Swap the bytes with NO broker touched. A daemonless swap can never leave\n    //    a running old broker paired with new brain bytes.\n    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\n        Ok(O::NoUpdate) => {\n            println!(\"NO_UPDATE\");\n            return 0;\n        }\n        // The proceed set — bytes are (or already were) on disk; go restart onto them.\n        Ok(o @ O::AppliedDaemonless { .. })\n        | Ok(o @ O::AppliedPending { .. })\n        | Ok(o @ O::AlreadyApplied { .. }) => o,\n        // The fail-closed refusals — nothing swapped, do NOT restart.\n        Ok(refused) => return report_apply_outcome(refused),\n        Err(e) => {\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\n            return 1;\n        }\n    };\n\n    // 2. Restart the daemon onto the new bytes. Announce the sessions the cycle\n    //    will bounce (they come back on start), stop if up, WAIT for it to be\n    //    actually down (else `start` would see it \"already running\" and skip the\n    //    swap onto new bytes), then start.\n    if spt_daemon::is_running() {\n        let hosted = live_hosted_session_ids();\n        if !hosted.is_empty() {\n            eprintln!(\n                \"Cycling {} hosted session(s) onto the new version (they come back automatically): {}\",\n                hosted.len(),\n                hosted.join(\", \")\n            );\n        }\n        if let Err(e) = spt_daemon::stop_daemon() {\n            eprintln!(\n                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n            );\n            return 1;\n        }\n        let down_by = std::time::Instant::now() + std::time::Duration::from_secs(10);\n        while spt_daemon::is_running() && std::time::Instant::now() < down_by {\n            std::thread::sleep(std::time::Duration::from_millis(50));\n        }\n        if spt_daemon::is_running() {\n            eprintln!(\n                \"DAEMON_STOP_TIMEOUT: the daemon did not stop within 10s — the new version is on \\\n                 disk; run `spt daemon start` once it stops to finish.\"\n            );\n            return 1;\n        }\n    }\n    match spt_daemon::start_daemon() {\n        Ok(_) => {\n            let (version, product_version, already) = match applied {\n                O::AppliedDaemonless {\n                    version,\n                    product_version,\n                }\n                | O::AppliedPending {\n…\n                } => (version, product_version, false),\n…\n            };\n…\n        }\n…\n    }\n}","startLine":158,"lineNumbers":[158,null,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,null,1348,null,1353,null,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,null,3961,3962,null,3967,null,3970,3971,3972,3973,3974,3975,3976,3977,3978,3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3992,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,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,null,4848,null,4850,null,4884,null,4890,4891,4892,4893,4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4908,4909,4910,4911,4912,4913,4914,4915,4916,4917,4918,4919,4920,4921,4922,4923,4924,4925,4926,4927,4928,4929,4930,4931,4932,4933,4934,4935,4936,4937,4938,4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4950,4951,4952,4953,4954,4955,4956,4957,4958,4959,4960,4961,4962,4963,4964,4965,4966,4967,4968,4969,4970,4971,4972,4973,4974,4975,4976,4977,4978,4979,4980,4981,4982,4983,4984,4985,4986,4987,4988,4989,4990,4991,4992,4993,4994,4995,4996,4997,4998,4999,5000,5001,5002,5003,5004,5005,5006,5007,5008,5009,5010,5011,5012,5013,5014,5015,5016,5017,5018,5019,5020,5021,5022,5023,5024,5025,5026,5027,5028,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,5040,5041,5042,5043,5044,5045,5046,5047,5048,5049,5050,5051,5052,5053,5054,5055,5056,5057,5058,5059,5060,5061,5062,5063,5064,5065,5066,5067,5068,null,5070,null,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149,5150,5151,5152,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164,5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,5176,5177,5178,5179,5180,5181,5182,5183,5184,5185,5186,5187,5188,5189,5190,5191,5192,5193,5194,5195,5196,5197,5198,5199,5200,5201,5202,5203,5204,5205,5206,5207,5208,5209,5210,null,5213,null,5219,null,5225,null,5232,5233]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"}}},"isError":false,"timestamp":1784195766586}}
{"type":"message","id":"2a02193e","parentId":"705a4f8a","timestamp":"2026-07-16T09:56:06.611Z","message":{"role":"toolResult","toolCallId":"call_uUyo64qoBnHLLdKfDgNCWJhU|fc_0cc7a0c9a797f232016a58aab65e78819abb4bf0f2b32c9c06","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/tests/\n## access.rs\n 146|// unsolicited traffic again — store edits take effect mid-stream, no daemon\n*147|// restart.\n 148|#[test]\n 149|fn whitelist_refuses_exempts_replies_and_admits_listed() {\n 150|    init_home();\n...\n 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 13|//!   on the stream table's handshake-proven `remote_id_hex`;\n*14|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n 15|//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n 16|//!   log, and the effect journal. The successor brain re-serves the SAME\n 17|//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n...\n 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n*29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n 32|#[cfg(unix)]\n...\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 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...\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 849|    let state = life1.snapshot();\n*850|    drop(life1); // the crash — broker A, session, stream, journal all survive\n 851|\n 852|    // Into the DEAD window: B types the second marker (buffered by A's broker).\n 853|    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\n...\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...\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 909|        \"the operator's rendered screen shows the dead-window marker delivered after \\\n*910|         the handoff (buffered input survived the restart): {painted:?}\"\n 911|    );\n 912|\n 913|    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\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 1038|\n 1039|use spt_daemon::msg::SubscribeOutcome;\n 1040|\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## brain_decouple.rs\n*1|//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n 2|//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n 3|//!\n*4|//! ## The bug this gate locks down (the update-wedge root)\n 5|//!\n*6|//! A controller's writer thread does a BLOCKING socket write to its brain\n 7|//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n 8|//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n*9|//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n*10|//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n*11|//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n 12|//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n 13|//! `daemon status` still read healthy.\n 14|//!\n...\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 32|//!    busy-refused forever);\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 177|                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\n*178|                    // Keep the conn briefly so a Controller take is not immediately\n 179|                    // detached (drop would clear the slot) — then let it fall.\n 180|                    thread::sleep(Duration::from_millis(200));\n 181|                    let _ = tx.send(Some(reply.outcome));\n...\n 219|\n*220|// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 221|#[test]\n*222|fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223|    init_home();\n 224|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225|    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n*226|    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\n 227|    // stall-evict can free the slot in the window this test acts in.\n 228|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 229|\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## brain_resume_conn_deadlock.rs\n*1|//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n*2|//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n 3|//!\n*4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n 5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n 6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n*8|//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n 9|//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n 10|//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n 11|//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n...\n 30|//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n*31|//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n 32|\n 33|use std::sync::atomic::{AtomicU32, Ordering};\n 34|use std::sync::{Arc, OnceLock};\n...\n 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...\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 235|\n*236|/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\n 237|/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\n 238|/// rate (the sessions predate this generation). The new generation's heartbeat — hence\n 239|/// its readiness/promotion — is never starved.\n...\n 259|/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\n*260|/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\n 261|/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\n 262|/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\n 263|/// ZERO subscribers.\n...\n 266|///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n*267|///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268|///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269|///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n 270|///   still a broken resume, just a different failure. Cursor-only must attach nothing.\n...\n 272|///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n*273|///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\n 274|///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\n 275|///   against an always-0 seam).\n 276|///\n...\n 281|/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\n*282|/// the broker dispatches that conn's frames strictly in order and inserts each viewer\n*283|/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\n 284|/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\n*285|/// also DRAINS the interleaved replay frames, so the broker's viewer writers never block\n 286|/// — no deadlock, and (at N=3) no write is held long enough for the r4 watchdog to\n*287|/// retire/evict a viewer before we read the counts. State is settled the moment the\n 288|/// barrier `sessions()` returns.\n 289|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 290|#[test]\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...\n 318|        assert_eq!(\n*319|            broker.test_session_viewer_count(sid),\n 320|            Some(0),\n*321|            \"the daemon's cursor-only resume must attach ZERO viewers to session {sid}; a \\\n 322|             non-zero count means the brain SUBSCRIBED its own undrained heartbeat conn — \\\n 323|             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\n 324|             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\n## brain_swap.rs\n*1|//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n*2|//! through the update engine with **zero hosted-endpoint interruption**\n 3|//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n 4|//!\n 5|//! This rides the same broker/brain substrate the B2 handoff test proves, but\n*6|//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n 7|//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n 8|//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n 9|//!\n...\n 11|//! brain injects a marker it never reads, then the engine applies a brain-only\n*12|//! update: the new brain re-attaches via the supplied relaunch closure (which\n 13|//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n 14|//! that marker in the *replayed* buffer — proving the broker buffered output\n 15|//! produced while no brain was attached. Throughout, the child's pid is unchanged\n...\n 17|//!\n*18|//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n 19|//! Windows' installer-detection heuristic (UAC) forces elevation for any\n*20|//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n*21|//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n 22|//! elevation). Do not rename this to match the module under test.\n 23|//!\n 24|//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n...\n 27|//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n*28|//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n 29|//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n 30|//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n 31|//! no longer the REQ-UPD-3 int tag.\n...\n 39|use spt_daemon::msg::SpawnReq;\n*40|use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n 41|use spt_daemon::Broker;\n 42|\n 43|static SEQ: AtomicU32 = AtomicU32::new(0);\n...\n 94|#[test]\n*95|fn brain_only_update_swaps_logic_with_zero_endpoint_interruption() {\n 96|    let name = unique_name();\n 97|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 98|\n...\n 122|    };\n*123|    let plan = plan_update(&running, release);\n 124|    assert_eq!(\n 125|        plan.class,\n*126|        UpdateClass::BrainOnly,\n 127|        \"a pure-logic release is brain-only\"\n 128|    );\n 129|\n...\n 133|    brain\n*134|        .send_input(b\"UPDATED_MARKER_DONE\\r\")\n 135|        .expect(\"inject marker for successor\");\n 136|\n*137|    // Apply the brain-only update through the engine. This is the swap: snapshot\n 138|    // → drop the outgoing brain (the crash boundary the broker tolerates) →\n 139|    // relaunch the new brain over the substrate. The engine touches no endpoint.\n 140|    now += 1; // wall clock advances so the new generation gets a fresh gen_start\n 141|    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))\n*142|        .expect(\"brain-only update must apply\");\n 143|\n 144|    // ── REQ-UPD-3: the hosted endpoint never noticed the swap. ──\n 145|    assert_eq!(\n...\n 147|        1,\n*148|        \"exactly one session across the update\"\n 149|    );\n 150|    assert_eq!(\n 151|        broker.session_pid(sid),\n...\n 164|\n*165|    // Gapless resume: the marker the pre-update brain injected (and never read)\n 166|    // arrives via the broker's replayed buffer — output produced across the swap\n 167|    // window was not lost.\n 168|    let mut buf = Vec::new();\n 169|    brain\n*170|        .read_output_until(b\"UPDATED_MARKER_DONE\", &mut buf)\n*171|        .expect(\"the swapped-in brain must replay output gaplessly across the update\");\n 172|\n*173|    // The post-update brain still fully drives the same endpoint: kill it and see\n 174|    // the exit event + reaping — the session was live and owned, not a husk.\n 175|    brain.kill_session().expect(\"kill session\");\n 176|    loop {\n## broker.rs\n 981|\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 1118|    for floor in [1u64, 0u64] {\n 1119|        send(\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 1213|\n*1214|// [int->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\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## conn_blackhole_lifecycle.rs\n*1|//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n 2|//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n 3|//! hosting REAL PTY children over the real local IPC.\n 4|//!\n...\n 9|//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n*10|//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n 11|//! finite burst floods its socket, so the broker's writer parks inside\n 12|//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n 13|//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n...\n 16|//!\n*17|//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n 18|//!      receiving output throughout the black-hole incident (the fan-out never\n 19|//!      blocks the whole broker on the one wedged conn);\n 20|//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n...\n 24|//!   3. ITS WRITER EXITS — `broker.conn_handler_count()` falls to the\n*25|//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n 26|//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n 27|//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n*28|//!      completion), and the viewer flows output AFTER the sample latches — proving\n*29|//!      the survivor is the viewer's handler, not a leaked one;\n*30|//!   4. A FRESH VIEWER CAN ATTACH — a brand-new Viewer subscribe on the retired\n 31|//!      session COMPLETES and RECEIVES replayed output from the frozen cursor\n 32|//!      (the fan-out accepts + serves a new subscriber after retirement);\n 33|//!   5. NO LOCK OR TASK REMAINS OWNED BY THE RETIRED CONNECTION — asserted via\n...\n 36|//!      is held by the retired writer), `conn_handler_count()` returns to the\n*37|//!      unrelated-viewer floor without the client dropping its end (no task\n 38|//!      lingers), AND both the\n*39|//!      unrelated session's viewer and the fresh viewer flow output (the shared\n 40|//!      fan-out lock is free). The conjunction of these three is the strongest\n 41|//!      deterministic proxy available for \"nothing is still owned.\"\n 42|//!\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 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n*54|//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n*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 135|/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n*136|/// session so its viewer has a continuous frame stream to keep ticking on\n 137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n...\n 157|/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n*158|/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n*159|/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n 160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n...\n 222|/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n...\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 268|    drop(u_spawn);\n 269|    let u_name = name.clone();\n...\n 279|                from_seq: 0,\n*280|                intent: AttachIntent::Viewer,\n## control_stamp_lifetime.rs\n*1|//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\n 2|//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\n 3|//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\n*4|//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\n*5|//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\n*6|//! gossiped a stale `controller_node` cross-node.\n 7|//!\n 8|//! ## The bug this gate locks down\n 9|//!\n...\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n*12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n*13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n*14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n 16|//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n*17|//! `controller_node=self`.\n 18|//!\n 19|//! ## What the fix does\n 20|//!\n...\n 22|//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\n*23|//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\n*24|//! writer; the session is dead so no live controller re-stamps concurrently (the\n 25|//! same race-free argument as the W5 no-session self-heal).\n 26|//!\n 27|//! ## Why kill the CHILD (not drop the conn)\n 28|//!\n*29|//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\n 30|//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n*31|//! while the controller conn stays open — isolating the reap clear as the ONLY\n 32|//! path that can unlatch the stamps.\n 33|//!\n 34|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n...\n 134|#[test]\n*135|fn reap_clears_control_and_viewer_stamps() {\n 136|    init_stamp_home();\n 137|    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138|    let id = format!(\"ctlstamp-{}\", std::process::id());\n...\n 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...\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*164|    // info.json: driven_by=Some(remote) + viewer_count=2 (broker is the single\n 165|    // writer of both; here we set the on-disk state the reap must unlatch). The\n 166|    // spawner stays attached, idle → nothing re-stamps these before the reap.\n 167|    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\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*20|//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n 21|//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n 22|//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n 23|//!    marker ([`REQ-DAEMON-4`]).\n...\n 37|// daemon-hosted-liveness), which it still exercises directly.\n*38|// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n 39|// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n*40|// brain-restart survival, not the process-level swap. Process-level endpoint\n*41|// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n 42|// [int->REQ-DAEMON-4]\n 43|// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n 44|\n...\n 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 222|        .send_input(b\"MARKER_1_DONE\\r\")\n...\n 226|\n*227|    for k in 1..=RESTARTS {\n 228|        now += 1; // advance wall clock so a fresh gen_start is strictly greater\n 229|        let prior_gen_start = state.gen_start_ms;\n 230|        let prior_generation = state.generation;\n...\n 245|\n*246|        // The child + its PTY survived the restart untouched.\n 247|        assert_eq!(\n 248|            broker.session_count(),\n 249|            1,\n*250|            \"exactly one session across restarts\"\n 251|        );\n 252|        assert_eq!(\n 253|            broker.session_pid(sid),\n 254|            child_pid,\n*255|            \"child pid stable across restart\"\n 256|        );\n 257|\n 258|        // Gapless resume: the marker the previous brain injected (and died before\n...\n 265|        // The daemon-hosted lifecycle is stateless on disk, so it keeps ingesting\n*266|        // straight across the brain restart: drop a commune, tick, confirm.\n 267|        std::fs::write(drops.path().join(\"doyle-commune.md\"), format!(\"brief {k}\")).unwrap();\n 268|        let rep = host\n 269|            .pulse_tick(Some(\"sid-1\"))\n*270|            .expect(\"pulse tick across restart\");\n 271|        assert_eq!(\n 272|            rep.ingested.len(),\n 273|            1,\n*274|            \"lifecycle keeps ingesting across the brain restart\"\n 275|        );\n 276|\n 277|        // Hand the next marker to our successor, then crash without reading it.\n*278|        if k < RESTARTS {\n 279|            brain\n 280|                .send_input(format!(\"MARKER_{}_DONE\\r\", k + 1).as_bytes())\n 281|                .expect(\"inject next marker\");\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## dispatch.rs\n 22|use spt_daemon::nethost::{NetConfig, NetHost};\n*23|use spt_daemon::propagate::{request_update, UpdatePullOutcome};\n 24|use spt_daemon::relcache::ReleaseCache;\n 25|use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease, VerifyPolicy};\n 26|use spt_daemon::sync::request_sync;\n*27|use spt_daemon::update::{BrokerAbi, BROKER_RESOURCE_ABI};\n 28|use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n 29|use spt_net::net::notif::NotifRecord;\n 30|use spt_net::net::wanmsg::WanMessage;\n...\n 123|    // Mesh-D6: membership is the roster — seed the canonical roster (the inbound\n*124|    // gates and the update serve gate both read it).\n 125|    let mut roster = spt_store::roster::RosterStore::load();\n 126|    roster.merge_entry(spt_store::roster::RosterEntry {\n 127|        pubkey_hex: peer_hex.to_string(),\n...\n 207|\n*208|// [int->REQ-UPD-1] an update pull is served end to end by the dispatcher:\n 209|// B stages a release, A queries/fetches/verifies/stages — no hand-wired\n*210|// serve_update. The serve gate ran from B's canonical roster.\n 211|#[test]\n*212|fn dispatcher_serves_an_update_pull_undriven() {\n 213|    init_home();\n 214|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 215|    let dir = TempDir::new().unwrap();\n...\n 262|    let cache_a = ReleaseCache::open(&dir.path().join(\"releases-a\"));\n*263|    let outcome = request_update(\n 264|        &mut a,\n 265|        conn.conn_id,\n 266|        MintedOp::new(Minter::Pump, op()),\n...\n 274|    assert!(\n*275|        matches!(outcome, UpdatePullOutcome::Updated(_)),\n*276|        \"the dispatcher served the update undriven: {outcome:?}\"\n 277|    );\n 278|    assert_eq!(\n 279|        cache_a.load_artifact().expect(\"staged bytes\"),\n...\n 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 1038|// [unit->REQ-SUBNET-3] the demux routes a node-label feed: an endpoint-less\n*1039|// peer sends a NodeLabelUpdate first (no instance row), so the feed's FIRST\n 1040|// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n 1041|// family (same serve arm + gate) rather than dropping as Unknown. An instance\n 1042|// row still classifies Registry; a bare unrelated object stays Unknown.\n...\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*1049|    let label = RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\n 1050|        subnet: \"home\".into(),\n 1051|        node: \"n1\".into(),\n 1052|        label: \"HFENDULEAM\".into(),\n...\n 1062|\n*1063|    let inst = RegistryFeedRecord::Instance(RegistryUpdate {\n 1064|        subnet: \"home\".into(),\n 1065|        endpoint_id: \"doyle\".into(),\n 1066|        instance: Instance {\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...\n 1094|#[test]\n*1095|fn dispatcher_survives_an_unknown_stream() {\n 1096|    init_home();\n 1097|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 1098|    let dir = TempDir::new().unwrap();\n...\n 1152|        found,\n*1153|        \"the dispatcher survived the unknown stream and served the next one\"\n 1154|    );\n 1155|    stop.store(true, Ordering::Relaxed);\n 1156|}\n## docs_server_e2e.rs\n 5|//! rejected; a docs-less home answers the loud not-found WITH the\n*6|//! `spt update fetch --apply` pointer while the server stays bound; writes\n 7|//! are refused (GET/HEAD only).\n 8|//!\n 9|//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n...\n 114|    assert!(\n*115|        text.contains(\"spt update fetch --apply\"),\n 116|        \"the pointer at the landing command: {text}\"\n 117|    );\n 118|    // Still bound and answering after the docs-less response.\n## driven_by_selfheal.rs\n 6|//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n*7|//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n 8|//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n*9|//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n 10|//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n 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 257|#[test]\n*258|fn gap_a_live_session_controller_by_is_ambiguous_so_reconcile_must_not_clear() {\n 259|    init_selfheal_home();\n 260|    let node = \"PEERNODEHEX_GAP_A\";\n 261|    let id = format!(\"w5-gapa-{}\", std::process::id());\n...\n 270|    // ── A REAL broker hosting a LIVE session for THIS endpoint. The spawning\n*271|    //    brain is pre-attached as the LOCAL controller (dispatch_spawn →\n*272|    //    become_controller(by=None)). The PTY child keeps the session live in the\n 273|    //    broker table. ──\n 274|    let name = unique_name();\n 275|    let broker = start_broker(&name);\n## endpoint_survival.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n 2|//! survival, against a REAL broker over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n 5|//!\n*6|//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\n 7|//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\n*8|//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n*9|//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n 10|//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n*11|//! endpoint the restart orphaned, resuming from its last ledger session.\n 12|//!\n 13|//! ## What this rig proves\n 14|//!\n...\n 18|//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n*19|//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n 20|//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n 21|//!\n 22|//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n...\n 57|/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n*58|/// ledger row to resume from — the daemon-restart-orphaned shape.\n 59|fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\n 60|    use spt_store::perch::{resolve_perch_path, resolve_ready_file, ParentHint};\n 61|    let perch = resolve_perch_path(id, ParentHint::Infer);\n...\n 84|\n*85|// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 86|#[test]\n*87|fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n 88|    let home = tempfile::TempDir::new().expect(\"temp home\");\n 89|    std::env::set_var(\"SPT_HOME\", home.path());\n 90|\n...\n 117|\n*118|    // The pass under test — the one-shot daemon-restart survival re-run.\n 119|    let owlery = spt_store::perch::owlery_dir();\n 120|    let adapters_dir = spt_store::perch::adapters_dir();\n 121|    let registered = spt_runtime::registry::registered(&adapters_dir);\n*122|    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 123|\n 124|    // The endpoint's harness was re-run → a live broker session now exists for it.\n 125|    let mut appeared = false;\n...\n 143|        appeared,\n*144|        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\n 145|         (a live broker session appears for it). RED without the pass: no session.\"\n 146|    );\n 147|}\n## false_promote.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\n 2|//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n 3|//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n 4|//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n...\n 7|//!\n*8|//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\n 9|//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\n 10|//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n*11|//! controller conn is still wedged (blocked writes pending), so `update apply`\n 12|//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n 13|//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n*14|//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n 15|//! read from BROKER TRUTH in-process (no brain round-trip).\n 16|//!\n 17|//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n...\n 25|//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n*26|//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n 27|//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\n*28|//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n 29|//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n 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 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 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 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*235|// [int->REQ-UPDATE-PROMOTE-DRAINED]\n 236|#[test]\n 237|fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238|    init_home();\n...\n 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## handoff.rs\n 1|//! B2 regression — the milestone's load-bearing proof: a brain can be killed and\n*2|//! restarted repeatedly while the broker and its hosted child survive untouched,\n 3|//! and each new brain re-attaches to the output stream **gaplessly** with a fresh\n\n[Showing lines 1-956 of 1197 (49.9KB limit). Use :957 to continue]"}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":284,"fileCount":20,"files":["crates/spt-daemon/tests/access.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt-daemon/tests/driven_by_selfheal.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/idempotent.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/access.rs","count":2},{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":20},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":20},{"path":"crates/spt-daemon/tests/brain_swap.rs","count":20},{"path":"crates/spt-daemon/tests/broker.rs","count":19},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":20},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":20},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":17},{"path":"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","count":2},{"path":"crates/spt-daemon/tests/dispatch.rs","count":17},{"path":"crates/spt-daemon/tests/docs_server_e2e.rs","count":2},{"path":"crates/spt-daemon/tests/driven_by_selfheal.rs","count":20},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":12},{"path":"crates/spt-daemon/tests/false_promote.rs","count":20},{"path":"crates/spt-daemon/tests/handoff.rs","count":9},{"path":"crates/spt-daemon/tests/idempotent.rs","count":6},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":20},{"path":"crates/spt-daemon/tests/input_ack_deadlock.rs","count":17},{"path":"crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/tests/\n## access.rs\n 146│// unsolicited traffic again — store edits take effect mid-stream, no daemon\n*147│// restart.\n 148│#[test]\n 149│fn whitelist_refuses_exempts_replies_and_admits_listed() {\n 150│    init_home();\n    │...\n 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   13│//!   on the stream table's handshake-proven `remote_id_hex`;\n  *14│//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n   15│//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n   16│//!   log, and the effect journal. The successor brain re-serves the SAME\n   17│//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n     │...\n   27│use spt_daemon::attach::{\n  *28│    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n  *29│    AttachServeOutcome, ControllerIrrecoverablyBehind,\n   30│};\n   31│// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n   32│#[cfg(unix)]\n     │...\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  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     │...\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  849│    let state = life1.snapshot();\n *850│    drop(life1); // the crash — broker A, session, stream, journal all survive\n  851│\n  852│    // Into the DEAD window: B types the second marker (buffered by A's broker).\n  853│    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\n     │...\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     │...\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  909│        \"the operator's rendered screen shows the dead-window marker delivered after \\\n *910│         the handoff (buffered input survived the restart): {painted:?}\"\n  911│    );\n  912│\n  913│    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\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 1038│\n 1039│use spt_daemon::msg::SubscribeOutcome;\n 1040│\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## brain_decouple.rs\n  *1│//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n   2│//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n   3│//!\n  *4│//! ## The bug this gate locks down (the update-wedge root)\n   5│//!\n  *6│//! A controller's writer thread does a BLOCKING socket write to its brain\n   7│//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n   8│//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n  *9│//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n *10│//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n *11│//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n  12│//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n  13│//! `daemon status` still read healthy.\n  14│//!\n    │...\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  32│//!    busy-refused forever);\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 177│                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\n*178│                    // Keep the conn briefly so a Controller take is not immediately\n 179│                    // detached (drop would clear the slot) — then let it fall.\n 180│                    thread::sleep(Duration::from_millis(200));\n 181│                    let _ = tx.send(Some(reply.outcome));\n    │...\n 219│\n*220│// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 221│#[test]\n*222│fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223│    init_home();\n 224│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225│    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n*226│    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\n 227│    // stall-evict can free the slot in the window this test acts in.\n 228│    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 229│\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## brain_resume_conn_deadlock.rs\n  *1│//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n  *2│//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n   3│//!\n  *4│//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n   5│//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n   6│//! onto the brain's OWN request/reply conn. A conn's send half is one\n   7│//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n  *8│//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n   9│//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n  10│//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n  11│//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n    │...\n  30│//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n *31│//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n  32│\n  33│use std::sync::atomic::{AtomicU32, Ordering};\n  34│use std::sync::{Arc, OnceLock};\n    │...\n 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    │...\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 235│\n*236│/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\n 237│/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\n 238│/// rate (the sessions predate this generation). The new generation's heartbeat — hence\n 239│/// its readiness/promotion — is never starved.\n    │...\n 259│/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\n*260│/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\n 261│/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\n 262│/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\n 263│/// ZERO subscribers.\n    │...\n 266│///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n*267│///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268│///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269│///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n 270│///   still a broken resume, just a different failure. Cursor-only must attach nothing.\n    │...\n 272│///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n*273│///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\n 274│///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\n 275│///   against an always-0 seam).\n 276│///\n    │...\n 281│/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\n*282│/// the broker dispatches that conn's frames strictly in order and inserts each viewer\n*283│/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\n 284│/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\n*285│/// also DRAINS the interleaved replay frames, so the broker's viewer writers never block\n 286│/// — no deadlock, and (at N=3) no write is held long enough for the r4 watchdog to\n*287│/// retire/evict a viewer before we read the counts. State is settled the moment the\n 288│/// barrier `sessions()` returns.\n 289│// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 290│#[test]\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    │...\n 318│        assert_eq!(\n*319│            broker.test_session_viewer_count(sid),\n 320│            Some(0),\n*321│            \"the daemon's cursor-only resume must attach ZERO viewers to session {sid}; a \\\n 322│             non-zero count means the brain SUBSCRIBED its own undrained heartbeat conn — \\\n 323│             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\n 324│             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\n## brain_swap.rs\n  *1│//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n  *2│//! through the update engine with **zero hosted-endpoint interruption**\n   3│//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n   4│//!\n   5│//! This rides the same broker/brain substrate the B2 handoff test proves, but\n  *6│//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n   7│//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n   8│//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n   9│//!\n    │...\n  11│//! brain injects a marker it never reads, then the engine applies a brain-only\n *12│//! update: the new brain re-attaches via the supplied relaunch closure (which\n  13│//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n  14│//! that marker in the *replayed* buffer — proving the broker buffered output\n  15│//! produced while no brain was attached. Throughout, the child's pid is unchanged\n    │...\n  17│//!\n *18│//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n  19│//! Windows' installer-detection heuristic (UAC) forces elevation for any\n *20│//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n *21│//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n  22│//! elevation). Do not rename this to match the module under test.\n  23│//!\n  24│//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n    │...\n  27│//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n *28│//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n  29│//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n  30│//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n  31│//! no longer the REQ-UPD-3 int tag.\n    │...\n  39│use spt_daemon::msg::SpawnReq;\n *40│use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n  41│use spt_daemon::Broker;\n  42│\n  43│static SEQ: AtomicU32 = AtomicU32::new(0);\n    │...\n  94│#[test]\n *95│fn brain_only_update_swaps_logic_with_zero_endpoint_interruption() {\n  96│    let name = unique_name();\n  97│    let broker = Broker::bind(&name).expect(\"bind broker\");\n  98│\n    │...\n 122│    };\n*123│    let plan = plan_update(&running, release);\n 124│    assert_eq!(\n 125│        plan.class,\n*126│        UpdateClass::BrainOnly,\n 127│        \"a pure-logic release is brain-only\"\n 128│    );\n 129│\n    │...\n 133│    brain\n*134│        .send_input(b\"UPDATED_MARKER_DONE\\r\")\n 135│        .expect(\"inject marker for successor\");\n 136│\n*137│    // Apply the brain-only update through the engine. This is the swap: snapshot\n 138│    // → drop the outgoing brain (the crash boundary the broker tolerates) →\n 139│    // relaunch the new brain over the substrate. The engine touches no endpoint.\n 140│    now += 1; // wall clock advances so the new generation gets a fresh gen_start\n 141│    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))\n*142│        .expect(\"brain-only update must apply\");\n 143│\n 144│    // ── REQ-UPD-3: the hosted endpoint never noticed the swap. ──\n 145│    assert_eq!(\n    │...\n 147│        1,\n*148│        \"exactly one session across the update\"\n 149│    );\n 150│    assert_eq!(\n 151│        broker.session_pid(sid),\n    │...\n 164│\n*165│    // Gapless resume: the marker the pre-update brain injected (and never read)\n 166│    // arrives via the broker's replayed buffer — output produced across the swap\n 167│    // window was not lost.\n 168│    let mut buf = Vec::new();\n 169│    brain\n*170│        .read_output_until(b\"UPDATED_MARKER_DONE\", &mut buf)\n*171│        .expect(\"the swapped-in brain must replay output gaplessly across the update\");\n 172│\n*173│    // The post-update brain still fully drives the same endpoint: kill it and see\n 174│    // the exit event + reaping — the session was live and owned, not a husk.\n 175│    brain.kill_session().expect(\"kill session\");\n 176│    loop {\n## broker.rs\n  981│\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 1118│    for floor in [1u64, 0u64] {\n 1119│        send(\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 1213│\n*1214│// [int->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\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## conn_blackhole_lifecycle.rs\n  *1│//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n   2│//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n   3│//! hosting REAL PTY children over the real local IPC.\n   4│//!\n    │...\n   9│//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n *10│//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n  11│//! finite burst floods its socket, so the broker's writer parks inside\n  12│//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n  13│//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n    │...\n  16│//!\n *17│//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n  18│//!      receiving output throughout the black-hole incident (the fan-out never\n  19│//!      blocks the whole broker on the one wedged conn);\n  20│//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n    │...\n  24│//!   3. ITS WRITER EXITS — `broker.conn_handler_count()` falls to the\n *25│//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n  26│//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n  27│//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n *28│//!      completion), and the viewer flows output AFTER the sample latches — proving\n *29│//!      the survivor is the viewer's handler, not a leaked one;\n *30│//!   4. A FRESH VIEWER CAN ATTACH — a brand-new Viewer subscribe on the retired\n  31│//!      session COMPLETES and RECEIVES replayed output from the frozen cursor\n  32│//!      (the fan-out accepts + serves a new subscriber after retirement);\n  33│//!   5. NO LOCK OR TASK REMAINS OWNED BY THE RETIRED CONNECTION — asserted via\n    │...\n  36│//!      is held by the retired writer), `conn_handler_count()` returns to the\n *37│//!      unrelated-viewer floor without the client dropping its end (no task\n  38│//!      lingers), AND both the\n *39│//!      unrelated session's viewer and the fresh viewer flow output (the shared\n  40│//!      fan-out lock is free). The conjunction of these three is the strongest\n  41│//!      deterministic proxy available for \"nothing is still owned.\"\n  42│//!\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  53│//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n *54│//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n *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 135│/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n*136│/// session so its viewer has a continuous frame stream to keep ticking on\n 137│/// throughout the black-hole incident (invariant 1).\n 138│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139│    SpawnReq {\n    │...\n 157│/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n*158│/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n*159│/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n 160│/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    │...\n 222│/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223│/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224│/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n    │...\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 268│    drop(u_spawn);\n 269│    let u_name = name.clone();\n    │...\n 279│                from_seq: 0,\n*280│                intent: AttachIntent::Viewer,\n## control_stamp_lifetime.rs\n  *1│//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\n   2│//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\n   3│//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\n  *4│//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\n  *5│//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\n  *6│//! gossiped a stale `controller_node` cross-node.\n   7│//!\n   8│//! ## The bug this gate locks down\n   9│//!\n    │...\n  11│//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n *12│//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n *13│//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n *14│//! path dropped the OutputLog with the controller slot still occupied, so the\n  15│//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n  16│//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n *17│//! `controller_node=self`.\n  18│//!\n  19│//! ## What the fix does\n  20│//!\n    │...\n  22│//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\n *23│//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\n *24│//! writer; the session is dead so no live controller re-stamps concurrently (the\n  25│//! same race-free argument as the W5 no-session self-heal).\n  26│//!\n  27│//! ## Why kill the CHILD (not drop the conn)\n  28│//!\n *29│//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\n  30│//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n *31│//! while the controller conn stays open — isolating the reap clear as the ONLY\n  32│//! path that can unlatch the stamps.\n  33│//!\n  34│//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n    │...\n 134│#[test]\n*135│fn reap_clears_control_and_viewer_stamps() {\n 136│    init_stamp_home();\n 137│    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138│    let id = format!(\"ctlstamp-{}\", std::process::id());\n    │...\n 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    │...\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*164│    // info.json: driven_by=Some(remote) + viewer_count=2 (broker is the single\n 165│    // writer of both; here we set the on-disk state the reap must unlatch). The\n 166│    // spawner stays attached, idle → nothing re-stamps these before the reap.\n 167│    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\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 *20│//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n  21│//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n  22│//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n  23│//!    marker ([`REQ-DAEMON-4`]).\n    │...\n  37│// daemon-hosted-liveness), which it still exercises directly.\n *38│// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n  39│// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n *40│// brain-restart survival, not the process-level swap. Process-level endpoint\n *41│// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n  42│// [int->REQ-DAEMON-4]\n  43│// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n  44│\n    │...\n 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 222│        .send_input(b\"MARKER_1_DONE\\r\")\n    │...\n 226│\n*227│    for k in 1..=RESTARTS {\n 228│        now += 1; // advance wall clock so a fresh gen_start is strictly greater\n 229│        let prior_gen_start = state.gen_start_ms;\n 230│        let prior_generation = state.generation;\n    │...\n 245│\n*246│        // The child + its PTY survived the restart untouched.\n 247│        assert_eq!(\n 248│            broker.session_count(),\n 249│            1,\n*250│            \"exactly one session across restarts\"\n 251│        );\n 252│        assert_eq!(\n 253│            broker.session_pid(sid),\n 254│            child_pid,\n*255│            \"child pid stable across restart\"\n 256│        );\n 257│\n 258│        // Gapless resume: the marker the previous brain injected (and died before\n    │...\n 265│        // The daemon-hosted lifecycle is stateless on disk, so it keeps ingesting\n*266│        // straight across the brain restart: drop a commune, tick, confirm.\n 267│        std::fs::write(drops.path().join(\"doyle-commune.md\"), format!(\"brief {k}\")).unwrap();\n 268│        let rep = host\n 269│            .pulse_tick(Some(\"sid-1\"))\n*270│            .expect(\"pulse tick across restart\");\n 271│        assert_eq!(\n 272│            rep.ingested.len(),\n 273│            1,\n*274│            \"lifecycle keeps ingesting across the brain restart\"\n 275│        );\n 276│\n 277│        // Hand the next marker to our successor, then crash without reading it.\n*278│        if k < RESTARTS {\n 279│            brain\n 280│                .send_input(format!(\"MARKER_{}_DONE\\r\", k + 1).as_bytes())\n 281│                .expect(\"inject next marker\");\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## dispatch.rs\n   22│use spt_daemon::nethost::{NetConfig, NetHost};\n  *23│use spt_daemon::propagate::{request_update, UpdatePullOutcome};\n   24│use spt_daemon::relcache::ReleaseCache;\n   25│use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease, VerifyPolicy};\n   26│use spt_daemon::sync::request_sync;\n  *27│use spt_daemon::update::{BrokerAbi, BROKER_RESOURCE_ABI};\n   28│use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n   29│use spt_net::net::notif::NotifRecord;\n   30│use spt_net::net::wanmsg::WanMessage;\n     │...\n  123│    // Mesh-D6: membership is the roster — seed the canonical roster (the inbound\n *124│    // gates and the update serve gate both read it).\n  125│    let mut roster = spt_store::roster::RosterStore::load();\n  126│    roster.merge_entry(spt_store::roster::RosterEntry {\n  127│        pubkey_hex: peer_hex.to_string(),\n     │...\n  207│\n *208│// [int->REQ-UPD-1] an update pull is served end to end by the dispatcher:\n  209│// B stages a release, A queries/fetches/verifies/stages — no hand-wired\n *210│// serve_update. The serve gate ran from B's canonical roster.\n  211│#[test]\n *212│fn dispatcher_serves_an_update_pull_undriven() {\n  213│    init_home();\n  214│    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n  215│    let dir = TempDir::new().unwrap();\n     │...\n  262│    let cache_a = ReleaseCache::open(&dir.path().join(\"releases-a\"));\n *263│    let outcome = request_update(\n  264│        &mut a,\n  265│        conn.conn_id,\n  266│        MintedOp::new(Minter::Pump, op()),\n     │...\n  274│    assert!(\n *275│        matches!(outcome, UpdatePullOutcome::Updated(_)),\n *276│        \"the dispatcher served the update undriven: {outcome:?}\"\n  277│    );\n  278│    assert_eq!(\n  279│        cache_a.load_artifact().expect(\"staged bytes\"),\n     │...\n  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 1038│// [unit->REQ-SUBNET-3] the demux routes a node-label feed: an endpoint-less\n*1039│// peer sends a NodeLabelUpdate first (no instance row), so the feed's FIRST\n 1040│// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n 1041│// family (same serve arm + gate) rather than dropping as Unknown. An instance\n 1042│// row still classifies Registry; a bare unrelated object stays Unknown.\n     │...\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*1049│    let label = RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\n 1050│        subnet: \"home\".into(),\n 1051│        node: \"n1\".into(),\n 1052│        label: \"HFENDULEAM\".into(),\n     │...\n 1062│\n*1063│    let inst = RegistryFeedRecord::Instance(RegistryUpdate {\n 1064│        subnet: \"home\".into(),\n 1065│        endpoint_id: \"doyle\".into(),\n 1066│        instance: Instance {\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     │...\n 1094│#[test]\n*1095│fn dispatcher_survives_an_unknown_stream() {\n 1096│    init_home();\n 1097│    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 1098│    let dir = TempDir::new().unwrap();\n     │...\n 1152│        found,\n*1153│        \"the dispatcher survived the unknown stream and served the next one\"\n 1154│    );\n 1155│    stop.store(true, Ordering::Relaxed);\n 1156│}\n## docs_server_e2e.rs\n   5│//! rejected; a docs-less home answers the loud not-found WITH the\n  *6│//! `spt update fetch --apply` pointer while the server stays bound; writes\n   7│//! are refused (GET/HEAD only).\n   8│//!\n   9│//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n    │...\n 114│    assert!(\n*115│        text.contains(\"spt update fetch --apply\"),\n 116│        \"the pointer at the landing command: {text}\"\n 117│    );\n 118│    // Still bound and answering after the docs-less response.\n## driven_by_selfheal.rs\n   6│//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n  *7│//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n   8│//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n  *9│//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n  10│//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n  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 257│#[test]\n*258│fn gap_a_live_session_controller_by_is_ambiguous_so_reconcile_must_not_clear() {\n 259│    init_selfheal_home();\n 260│    let node = \"PEERNODEHEX_GAP_A\";\n 261│    let id = format!(\"w5-gapa-{}\", std::process::id());\n    │...\n 270│    // ── A REAL broker hosting a LIVE session for THIS endpoint. The spawning\n*271│    //    brain is pre-attached as the LOCAL controller (dispatch_spawn →\n*272│    //    become_controller(by=None)). The PTY child keeps the session live in the\n 273│    //    broker table. ──\n 274│    let name = unique_name();\n 275│    let broker = start_broker(&name);\n## endpoint_survival.rs\n  *1│//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n   2│//! survival, against a REAL broker over the real local IPC.\n   3│//!\n   4│//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n   5│//!\n  *6│//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\n   7│//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\n  *8│//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n  *9│//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n  10│//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n *11│//! endpoint the restart orphaned, resuming from its last ledger session.\n  12│//!\n  13│//! ## What this rig proves\n  14│//!\n    │...\n  18│//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n *19│//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n  20│//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n  21│//!\n  22│//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n    │...\n  57│/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n *58│/// ledger row to resume from — the daemon-restart-orphaned shape.\n  59│fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\n  60│    use spt_store::perch::{resolve_perch_path, resolve_ready_file, ParentHint};\n  61│    let perch = resolve_perch_path(id, ParentHint::Infer);\n    │...\n  84│\n *85│// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n  86│#[test]\n *87│fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n  88│    let home = tempfile::TempDir::new().expect(\"temp home\");\n  89│    std::env::set_var(\"SPT_HOME\", home.path());\n  90│\n    │...\n 117│\n*118│    // The pass under test — the one-shot daemon-restart survival re-run.\n 119│    let owlery = spt_store::perch::owlery_dir();\n 120│    let adapters_dir = spt_store::perch::adapters_dir();\n 121│    let registered = spt_runtime::registry::registered(&adapters_dir);\n*122│    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 123│\n 124│    // The endpoint's harness was re-run → a live broker session now exists for it.\n 125│    let mut appeared = false;\n    │...\n 143│        appeared,\n*144│        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\n 145│         (a live broker session appears for it). RED without the pass: no session.\"\n 146│    );\n 147│}\n## false_promote.rs\n  *1│//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\n   2│//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n   3│//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n   4│//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n    │...\n   7│//!\n  *8│//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\n   9│//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\n  10│//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n *11│//! controller conn is still wedged (blocked writes pending), so `update apply`\n  12│//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n  13│//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n *14│//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n  15│//! read from BROKER TRUTH in-process (no brain round-trip).\n  16│//!\n  17│//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n    │...\n  25│//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n *26│//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n  27│//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\n *28│//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n  29│//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n  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  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  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 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*235│// [int->REQ-UPDATE-PROMOTE-DRAINED]\n 236│#[test]\n 237│fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238│    init_home();\n    │...\n 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## handoff.rs\n   1│//! B2 regression — the milestone's load-bearing proof: a brain can be killed and\n  *2│//! restarted repeatedly while the broker and its hosted child survive untouched,\n   3│//! and each new brain re-attaches to the output stream **gaplessly** with a fresh\n   4│//! `gen_start` (Spike #1 made real; KNOWN-HAZARDS 2.3 / 2.4).\n   5│//!\n    │...\n  18│// [int->REQ-HAZARD-GEN-START-NOW]\n *19│// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n  20│// D7-1): this proves the *in-process* handoff shape (a dropped connection as the\n  21│// crash boundary, brain driven in-thread) — the process-level survival onto a\n  22│// swapped binary is the D7-1 E2E. This file keeps proving the gen-start invariant.\n    │...\n  78│#[test]\n *79│fn brain_restart_survives_gaplessly_with_fresh_gen_start() {\n *80│    const RESTARTS: u64 = 12; // >= 10 (B2 acceptance)\n  81│    let name = unique_name();\n  82│    let broker = Broker::bind(&name).expect(\"bind broker\");\n  83│\n    │...\n 110│    let mut buf: Vec<u8> = Vec::new();\n*111│    for k in 1..=RESTARTS {\n 112│        now += 1; // advance wall clock so a fresh gen_start is strictly greater\n 113│        let mut brain = handoff_retry(&name, state.clone(), now);\n 114│\n    │...\n 133│\n*134│        // The child and its PTY survived the restart untouched.\n 135│        assert_eq!(\n 136│            broker.session_count(),\n 137│            1,\n*138│            \"exactly one session across restarts\"\n 139│        );\n 140│        assert_eq!(\n 141│            broker.session_pid(sid),\n 142│            child_pid,\n*143│            \"child pid stable across restart\"\n 144│        );\n 145│\n 146│        // Gapless resume: the marker the previous brain injected (and died before\n    │...\n 153│        // Hand the next marker to our successor, then crash without reading it.\n*154│        if k < RESTARTS {\n 155│            brain\n 156│                .send_input(format!(\"MARKER_{}_DONE\\r\", k + 1).as_bytes())\n 157│                .expect(\"inject next marker\");\n## idempotent.rs\n   2│//! PTY-write boundary over the versioned IPC (KNOWN-HAZARDS 7.2,\n  *3│//! [`REQ-HAZARD-RESTART-IDEMPOTENT`]; Spike #6 made real).\n   4│//!\n   5│//! A durable source of ordered input ops (each a unique line) is delivered to a\n   6│//! single echo child the broker hosts for the whole test. The brain is crashed\n    │...\n  25│//! in-thread and a dropped connection is the crash boundary (the broker + child\n *26│//! survive; the anchor persists).\n *27│// [int->REQ-HAZARD-RESTART-IDEMPOTENT]\n  28│\n  29│use std::sync::atomic::{AtomicU32, Ordering};\n  30│use std::sync::Arc;\n    │...\n 190│        let mut brain = handoff_retry(&name, state.clone(), now);\n*191│        // The child + its PTY survive every restart untouched.\n 192│        assert_eq!(\n 193│            broker.session_count(),\n 194│            1,\n*195│            \"exactly one session across restarts\"\n 196│        );\n 197│        assert_eq!(\n 198│            broker.session_pid(sid),\n 199│            child_pid,\n*200│            \"child pid stable across restart\"\n 201│        );\n 202│\n 203│        run_life(&mut brain, &mut buf, TOTAL, crash);\n## inject_control_wedge.rs\n  *1│//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY\n   2│//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local\n   3│//! IPC (no mocks of the PTY or the transport).\n   4│//!\n    │...\n   7│//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the\n  *8│//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the\n   9│//! session's single drain thread, while the `Mutex<OutputLog>` was locked.\n *10│//! VIEWERS instead get a dedicated writer thread + a bounded, evicting\n  11│//! `sync_channel` and can never stall the drain. So a slow / non-draining\n *12│//! controller (here a controller connection that stops reading while the child\n  13│//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output\n  14│//! stalled and any path needing that log lock (a fresh attach, or the global\n  15│//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)\n    │...\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 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 293│\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 329│    // A fresh connection's KIND_NET_STATUS touches neither the sessions nor the\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## 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 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 258│/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the keystone gate. A FLOOD of N=64 operator\n*259│/// `Input` records is driven through the REAL `serve_attach` CONTROLLER path on ONE\n 260│/// conn (the rc → net-stream → serve_attach → send_effect_no_ack route), exceeding\n 261│/// the ~10-frame IPC pipe buffer. Post-fix the broker emits NO applied frame for\n 262│/// the fire-and-forward path, so its per-conn handler never wedges; a CONCURRENT\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*292│    //    stdout so the concurrent viewer below has output to actually receive.\n 293│    //\n 294│    //    This whole leg runs on its own thread behind a result channel so a wedge\n 295│    //    (the pre-fix deadlock) surfaces as a watchdog timeout, never a hang. ──\n    │...\n 323│        let own_origin = dialed.remote_id_hex.clone();\n*324│        // CONTROLLER intent: this is the path the fix changed (the operator/rc\n 325│        // fire-and-forward route → send_effect_no_ack).\n 326│        let stream = match request_attach(\n 327│            &mut operator,\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 407│\n*408│    // ── (B) The CONCURRENT viewer attach — the non-vacuous proof the broker is NOT\n*409│    //    wedged. A SEPARATE operator dials loopback, requests a VIEWER attach,\n 410│    //    subscribes, and RECEIVES the flood child's output. Pre-fix the per-conn\n 411│    //    handler is deadlocked on the flood's ack stream and this subscribe is never\n 412│    //    serviced. Run on its own thread; the outer recv_timeout is the hard ceiling. ──\n    │...\n 437│            MintedOp::new(Minter::Rc, 2),\n*438│            AttachIntent::Viewer,\n 439│        ) {\n 440│            Ok(s) => s,\n 441│            Err(_) => {\n    │...\n 463│\n*464│        // Real byte receipt: the viewer must actually RECEIVE PTY output — here the\n 465│        // ECHOED flood input (`FLOODINPUT-…`) round-tripping back through the PTY.\n 466│        // Output delivery does not go through the input path, so receiving these\n 467│        // bytes proves the dispatch serviced this attach while the flood was driven.\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 20│//!   1. SWEEP direction — bypass the kill (early-return before `kill_pid_tree` in\n*21│//!      `sweep_legacy_resident_psyche_for`) → the mock SURVIVES + its residue stays →\n 22│//!      the \"swept + residue cleared\" asserts go RED.\n 23│//!   2. SPARE direction — bypass the cmdline gate in `legacy_psyche_should_reap`\n 24│//!      (drop the `<id>-psyche` `.contains` check, return `basename_ok`) → the control","truncation":{"content":"# crates/spt-daemon/tests/\n## access.rs\n 146|// unsolicited traffic again — store edits take effect mid-stream, no daemon\n*147|// restart.\n 148|#[test]\n 149|fn whitelist_refuses_exempts_replies_and_admits_listed() {\n 150|    init_home();\n...\n 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 13|//!   on the stream table's handshake-proven `remote_id_hex`;\n*14|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n 15|//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n 16|//!   log, and the effect journal. The successor brain re-serves the SAME\n 17|//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n...\n 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n*29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n 32|#[cfg(unix)]\n...\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 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...\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 849|    let state = life1.snapshot();\n*850|    drop(life1); // the crash — broker A, session, stream, journal all survive\n 851|\n 852|    // Into the DEAD window: B types the second marker (buffered by A's broker).\n 853|    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\n...\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...\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 909|        \"the operator's rendered screen shows the dead-window marker delivered after \\\n*910|         the handoff (buffered input survived the restart): {painted:?}\"\n 911|    );\n 912|\n 913|    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\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 1038|\n 1039|use spt_daemon::msg::SubscribeOutcome;\n 1040|\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## brain_decouple.rs\n*1|//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n 2|//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n 3|//!\n*4|//! ## The bug this gate locks down (the update-wedge root)\n 5|//!\n*6|//! A controller's writer thread does a BLOCKING socket write to its brain\n 7|//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n 8|//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n*9|//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n*10|//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n*11|//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n 12|//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n 13|//! `daemon status` still read healthy.\n 14|//!\n...\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 32|//!    busy-refused forever);\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 177|                    let reply: SubscribedReply = serde_json::from_value(f.payload).unwrap();\n*178|                    // Keep the conn briefly so a Controller take is not immediately\n 179|                    // detached (drop would clear the slot) — then let it fall.\n 180|                    thread::sleep(Duration::from_millis(200));\n 181|                    let _ = tx.send(Some(reply.outcome));\n...\n 219|\n*220|// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 221|#[test]\n*222|fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223|    init_home();\n 224|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225|    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n*226|    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\n 227|    // stall-evict can free the slot in the window this test acts in.\n 228|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 229|\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## brain_resume_conn_deadlock.rs\n*1|//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n*2|//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n 3|//!\n*4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n 5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n 6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n*8|//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n 9|//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n 10|//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n 11|//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n...\n 30|//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n*31|//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n 32|\n 33|use std::sync::atomic::{AtomicU32, Ordering};\n 34|use std::sync::{Arc, OnceLock};\n...\n 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...\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 235|\n*236|/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\n 237|/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\n 238|/// rate (the sessions predate this generation). The new generation's heartbeat — hence\n 239|/// its readiness/promotion — is never starved.\n...\n 259|/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\n*260|/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\n 261|/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\n 262|/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\n 263|/// ZERO subscribers.\n...\n 266|///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n*267|///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268|///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269|///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n 270|///   still a broken resume, just a different failure. Cursor-only must attach nothing.\n...\n 272|///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n*273|///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\n 274|///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\n 275|///   against an always-0 seam).\n 276|///\n...\n 281|/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\n*282|/// the broker dispatches that conn's frames strictly in order and inserts each viewer\n*283|/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\n 284|/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\n*285|/// also DRAINS the interleaved replay frames, so the broker's viewer writers never block\n 286|/// — no deadlock, and (at N=3) no write is held long enough for the r4 watchdog to\n*287|/// retire/evict a viewer before we read the counts. State is settled the moment the\n 288|/// barrier `sessions()` returns.\n 289|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 290|#[test]\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...\n 318|        assert_eq!(\n*319|            broker.test_session_viewer_count(sid),\n 320|            Some(0),\n*321|            \"the daemon's cursor-only resume must attach ZERO viewers to session {sid}; a \\\n 322|             non-zero count means the brain SUBSCRIBED its own undrained heartbeat conn — \\\n 323|             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\n 324|             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\n## brain_swap.rs\n*1|//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n*2|//! through the update engine with **zero hosted-endpoint interruption**\n 3|//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n 4|//!\n 5|//! This rides the same broker/brain substrate the B2 handoff test proves, but\n*6|//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n 7|//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n 8|//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n 9|//!\n...\n 11|//! brain injects a marker it never reads, then the engine applies a brain-only\n*12|//! update: the new brain re-attaches via the supplied relaunch closure (which\n 13|//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n 14|//! that marker in the *replayed* buffer — proving the broker buffered output\n 15|//! produced while no brain was attached. Throughout, the child's pid is unchanged\n...\n 17|//!\n*18|//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n 19|//! Windows' installer-detection heuristic (UAC) forces elevation for any\n*20|//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n*21|//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n 22|//! elevation). Do not rename this to match the module under test.\n 23|//!\n 24|//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n...\n 27|//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n*28|//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n 29|//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n 30|//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n 31|//! no longer the REQ-UPD-3 int tag.\n...\n 39|use spt_daemon::msg::SpawnReq;\n*40|use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n 41|use spt_daemon::Broker;\n 42|\n 43|static SEQ: AtomicU32 = AtomicU32::new(0);\n...\n 94|#[test]\n*95|fn brain_only_update_swaps_logic_with_zero_endpoint_interruption() {\n 96|    let name = unique_name();\n 97|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 98|\n...\n 122|    };\n*123|    let plan = plan_update(&running, release);\n 124|    assert_eq!(\n 125|        plan.class,\n*126|        UpdateClass::BrainOnly,\n 127|        \"a pure-logic release is brain-only\"\n 128|    );\n 129|\n...\n 133|    brain\n*134|        .send_input(b\"UPDATED_MARKER_DONE\\r\")\n 135|        .expect(\"inject marker for successor\");\n 136|\n*137|    // Apply the brain-only update through the engine. This is the swap: snapshot\n 138|    // → drop the outgoing brain (the crash boundary the broker tolerates) →\n 139|    // relaunch the new brain over the substrate. The engine touches no endpoint.\n 140|    now += 1; // wall clock advances so the new generation gets a fresh gen_start\n 141|    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))\n*142|        .expect(\"brain-only update must apply\");\n 143|\n 144|    // ── REQ-UPD-3: the hosted endpoint never noticed the swap. ──\n 145|    assert_eq!(\n...\n 147|        1,\n*148|        \"exactly one session across the update\"\n 149|    );\n 150|    assert_eq!(\n 151|        broker.session_pid(sid),\n...\n 164|\n*165|    // Gapless resume: the marker the pre-update brain injected (and never read)\n 166|    // arrives via the broker's replayed buffer — output produced across the swap\n 167|    // window was not lost.\n 168|    let mut buf = Vec::new();\n 169|    brain\n*170|        .read_output_until(b\"UPDATED_MARKER_DONE\", &mut buf)\n*171|        .expect(\"the swapped-in brain must replay output gaplessly across the update\");\n 172|\n*173|    // The post-update brain still fully drives the same endpoint: kill it and see\n 174|    // the exit event + reaping — the session was live and owned, not a husk.\n 175|    brain.kill_session().expect(\"kill session\");\n 176|    loop {\n## broker.rs\n 981|\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 1118|    for floor in [1u64, 0u64] {\n 1119|        send(\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 1213|\n*1214|// [int->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\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## conn_blackhole_lifecycle.rs\n*1|//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n 2|//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n 3|//! hosting REAL PTY children over the real local IPC.\n 4|//!\n...\n 9|//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n*10|//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n 11|//! finite burst floods its socket, so the broker's writer parks inside\n 12|//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n 13|//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n...\n 16|//!\n*17|//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n 18|//!      receiving output throughout the black-hole incident (the fan-out never\n 19|//!      blocks the whole broker on the one wedged conn);\n 20|//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n...\n 24|//!   3. ITS WRITER EXITS — `broker.conn_handler_count()` falls to the\n*25|//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n 26|//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n 27|//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n*28|//!      completion), and the viewer flows output AFTER the sample latches — proving\n*29|//!      the survivor is the viewer's handler, not a leaked one;\n*30|//!   4. A FRESH VIEWER CAN ATTACH — a brand-new Viewer subscribe on the retired\n 31|//!      session COMPLETES and RECEIVES replayed output from the frozen cursor\n 32|//!      (the fan-out accepts + serves a new subscriber after retirement);\n 33|//!   5. NO LOCK OR TASK REMAINS OWNED BY THE RETIRED CONNECTION — asserted via\n...\n 36|//!      is held by the retired writer), `conn_handler_count()` returns to the\n*37|//!      unrelated-viewer floor without the client dropping its end (no task\n 38|//!      lingers), AND both the\n*39|//!      unrelated session's viewer and the fresh viewer flow output (the shared\n 40|//!      fan-out lock is free). The conjunction of these three is the strongest\n 41|//!      deterministic proxy available for \"nothing is still owned.\"\n 42|//!\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 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n*54|//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n*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 135|/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n*136|/// session so its viewer has a continuous frame stream to keep ticking on\n 137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n...\n 157|/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n*158|/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n*159|/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n 160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n...\n 222|/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n...\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 268|    drop(u_spawn);\n 269|    let u_name = name.clone();\n...\n 279|                from_seq: 0,\n*280|                intent: AttachIntent::Viewer,\n## control_stamp_lifetime.rs\n*1|//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\n 2|//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\n 3|//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\n*4|//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\n*5|//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\n*6|//! gossiped a stale `controller_node` cross-node.\n 7|//!\n 8|//! ## The bug this gate locks down\n 9|//!\n...\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n*12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n*13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n*14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n 16|//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n*17|//! `controller_node=self`.\n 18|//!\n 19|//! ## What the fix does\n 20|//!\n...\n 22|//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\n*23|//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\n*24|//! writer; the session is dead so no live controller re-stamps concurrently (the\n 25|//! same race-free argument as the W5 no-session self-heal).\n 26|//!\n 27|//! ## Why kill the CHILD (not drop the conn)\n 28|//!\n*29|//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\n 30|//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n*31|//! while the controller conn stays open — isolating the reap clear as the ONLY\n 32|//! path that can unlatch the stamps.\n 33|//!\n 34|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n...\n 134|#[test]\n*135|fn reap_clears_control_and_viewer_stamps() {\n 136|    init_stamp_home();\n 137|    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138|    let id = format!(\"ctlstamp-{}\", std::process::id());\n...\n 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...\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*164|    // info.json: driven_by=Some(remote) + viewer_count=2 (broker is the single\n 165|    // writer of both; here we set the on-disk state the reap must unlatch). The\n 166|    // spawner stays attached, idle → nothing re-stamps these before the reap.\n 167|    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\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*20|//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n 21|//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n 22|//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n 23|//!    marker ([`REQ-DAEMON-4`]).\n...\n 37|// daemon-hosted-liveness), which it still exercises directly.\n*38|// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n 39|// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n*40|// brain-restart survival, not the process-level swap. Process-level endpoint\n*41|// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n 42|// [int->REQ-DAEMON-4]\n 43|// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n 44|\n...\n 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 222|        .send_input(b\"MARKER_1_DONE\\r\")\n...\n 226|\n*227|    for k in 1..=RESTARTS {\n 228|        now += 1; // advance wall clock so a fresh gen_start is strictly greater\n 229|        let prior_gen_start = state.gen_start_ms;\n 230|        let prior_generation = state.generation;\n...\n 245|\n*246|        // The child + its PTY survived the restart untouched.\n 247|        assert_eq!(\n 248|            broker.session_count(),\n 249|            1,\n*250|            \"exactly one session across restarts\"\n 251|        );\n 252|        assert_eq!(\n 253|            broker.session_pid(sid),\n 254|            child_pid,\n*255|            \"child pid stable across restart\"\n 256|        );\n 257|\n 258|        // Gapless resume: the marker the previous brain injected (and died before\n...\n 265|        // The daemon-hosted lifecycle is stateless on disk, so it keeps ingesting\n*266|        // straight across the brain restart: drop a commune, tick, confirm.\n 267|        std::fs::write(drops.path().join(\"doyle-commune.md\"), format!(\"brief {k}\")).unwrap();\n 268|        let rep = host\n 269|            .pulse_tick(Some(\"sid-1\"))\n*270|            .expect(\"pulse tick across restart\");\n 271|        assert_eq!(\n 272|            rep.ingested.len(),\n 273|            1,\n*274|            \"lifecycle keeps ingesting across the brain restart\"\n 275|        );\n 276|\n 277|        // Hand the next marker to our successor, then crash without reading it.\n*278|        if k < RESTARTS {\n 279|            brain\n 280|                .send_input(format!(\"MARKER_{}_DONE\\r\", k + 1).as_bytes())\n 281|                .expect(\"inject next marker\");\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## dispatch.rs\n 22|use spt_daemon::nethost::{NetConfig, NetHost};\n*23|use spt_daemon::propagate::{request_update, UpdatePullOutcome};\n 24|use spt_daemon::relcache::ReleaseCache;\n 25|use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease, VerifyPolicy};\n 26|use spt_daemon::sync::request_sync;\n*27|use spt_daemon::update::{BrokerAbi, BROKER_RESOURCE_ABI};\n 28|use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n 29|use spt_net::net::notif::NotifRecord;\n 30|use spt_net::net::wanmsg::WanMessage;\n...\n 123|    // Mesh-D6: membership is the roster — seed the canonical roster (the inbound\n*124|    // gates and the update serve gate both read it).\n 125|    let mut roster = spt_store::roster::RosterStore::load();\n 126|    roster.merge_entry(spt_store::roster::RosterEntry {\n 127|        pubkey_hex: peer_hex.to_string(),\n...\n 207|\n*208|// [int->REQ-UPD-1] an update pull is served end to end by the dispatcher:\n 209|// B stages a release, A queries/fetches/verifies/stages — no hand-wired\n*210|// serve_update. The serve gate ran from B's canonical roster.\n 211|#[test]\n*212|fn dispatcher_serves_an_update_pull_undriven() {\n 213|    init_home();\n 214|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 215|    let dir = TempDir::new().unwrap();\n...\n 262|    let cache_a = ReleaseCache::open(&dir.path().join(\"releases-a\"));\n*263|    let outcome = request_update(\n 264|        &mut a,\n 265|        conn.conn_id,\n 266|        MintedOp::new(Minter::Pump, op()),\n...\n 274|    assert!(\n*275|        matches!(outcome, UpdatePullOutcome::Updated(_)),\n*276|        \"the dispatcher served the update undriven: {outcome:?}\"\n 277|    );\n 278|    assert_eq!(\n 279|        cache_a.load_artifact().expect(\"staged bytes\"),\n...\n 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 1038|// [unit->REQ-SUBNET-3] the demux routes a node-label feed: an endpoint-less\n*1039|// peer sends a NodeLabelUpdate first (no instance row), so the feed's FIRST\n 1040|// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n 1041|// family (same serve arm + gate) rather than dropping as Unknown. An instance\n 1042|// row still classifies Registry; a bare unrelated object stays Unknown.\n...\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*1049|    let label = RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\n 1050|        subnet: \"home\".into(),\n 1051|        node: \"n1\".into(),\n 1052|        label: \"HFENDULEAM\".into(),\n...\n 1062|\n*1063|    let inst = RegistryFeedRecord::Instance(RegistryUpdate {\n 1064|        subnet: \"home\".into(),\n 1065|        endpoint_id: \"doyle\".into(),\n 1066|        instance: Instance {\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...\n 1094|#[test]\n*1095|fn dispatcher_survives_an_unknown_stream() {\n 1096|    init_home();\n 1097|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 1098|    let dir = TempDir::new().unwrap();\n...\n 1152|        found,\n*1153|        \"the dispatcher survived the unknown stream and served the next one\"\n 1154|    );\n 1155|    stop.store(true, Ordering::Relaxed);\n 1156|}\n## docs_server_e2e.rs\n 5|//! rejected; a docs-less home answers the loud not-found WITH the\n*6|//! `spt update fetch --apply` pointer while the server stays bound; writes\n 7|//! are refused (GET/HEAD only).\n 8|//!\n 9|//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n...\n 114|    assert!(\n*115|        text.contains(\"spt update fetch --apply\"),\n 116|        \"the pointer at the landing command: {text}\"\n 117|    );\n 118|    // Still bound and answering after the docs-less response.\n## driven_by_selfheal.rs\n 6|//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n*7|//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n 8|//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n*9|//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n 10|//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n 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 257|#[test]\n*258|fn gap_a_live_session_controller_by_is_ambiguous_so_reconcile_must_not_clear() {\n 259|    init_selfheal_home();\n 260|    let node = \"PEERNODEHEX_GAP_A\";\n 261|    let id = format!(\"w5-gapa-{}\", std::process::id());\n...\n 270|    // ── A REAL broker hosting a LIVE session for THIS endpoint. The spawning\n*271|    //    brain is pre-attached as the LOCAL controller (dispatch_spawn →\n*272|    //    become_controller(by=None)). The PTY child keeps the session live in the\n 273|    //    broker table. ──\n 274|    let name = unique_name();\n 275|    let broker = start_broker(&name);\n## endpoint_survival.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n 2|//! survival, against a REAL broker over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n 5|//!\n*6|//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\n 7|//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\n*8|//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n*9|//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n 10|//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n*11|//! endpoint the restart orphaned, resuming from its last ledger session.\n 12|//!\n 13|//! ## What this rig proves\n 14|//!\n...\n 18|//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n*19|//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n 20|//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n 21|//!\n 22|//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n...\n 57|/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n*58|/// ledger row to resume from — the daemon-restart-orphaned shape.\n 59|fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\n 60|    use spt_store::perch::{resolve_perch_path, resolve_ready_file, ParentHint};\n 61|    let perch = resolve_perch_path(id, ParentHint::Infer);\n...\n 84|\n*85|// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 86|#[test]\n*87|fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n 88|    let home = tempfile::TempDir::new().expect(\"temp home\");\n 89|    std::env::set_var(\"SPT_HOME\", home.path());\n 90|\n...\n 117|\n*118|    // The pass under test — the one-shot daemon-restart survival re-run.\n 119|    let owlery = spt_store::perch::owlery_dir();\n 120|    let adapters_dir = spt_store::perch::adapters_dir();\n 121|    let registered = spt_runtime::registry::registered(&adapters_dir);\n*122|    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 123|\n 124|    // The endpoint's harness was re-run → a live broker session now exists for it.\n 125|    let mut appeared = false;\n...\n 143|        appeared,\n*144|        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\n 145|         (a live broker session appears for it). RED without the pass: no session.\"\n 146|    );\n 147|}\n## false_promote.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\n 2|//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n 3|//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n 4|//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n...\n 7|//!\n*8|//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\n 9|//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\n 10|//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n*11|//! controller conn is still wedged (blocked writes pending), so `update apply`\n 12|//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n 13|//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n*14|//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n 15|//! read from BROKER TRUTH in-process (no brain round-trip).\n 16|//!\n 17|//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n...\n 25|//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n*26|//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n 27|//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\n*28|//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n 29|//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n 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 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 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 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*235|// [int->REQ-UPDATE-PROMOTE-DRAINED]\n 236|#[test]\n 237|fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238|    init_home();\n...\n 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## handoff.rs\n 1|//! B2 regression — the milestone's load-bearing proof: a brain can be killed and\n*2|//! restarted repeatedly while the broker and its hosted child survive untouched,\n 3|//! and each new brain re-attaches to the output stream **gaplessly** with a fresh","truncated":true,"truncatedBy":"bytes","totalLines":1197,"totalBytes":64285,"outputLines":956,"outputBytes":51147,"lastLinePartial":false,"firstLineExceedsLimit":false},"meta":{"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":1197,"totalBytes":64285,"outputLines":956,"outputBytes":51147,"shownRange":{"start":1,"end":956},"nextOffset":957}}},"isError":false,"timestamp":1784195766611}}
{"type":"message","id":"23ad99b5","parentId":"2a02193e","timestamp":"2026-07-16T09:56:06.614Z","message":{"role":"toolResult","toolCallId":"call_x7jEAPVirbNuk66fpoR5CtMX|fc_0cc7a0c9a797f232016a58aab65e6c819aad8ee44b53edd6d8","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### broker.rs\n 13|//! crash). The broker — and every hosted child — must survive that untouched,\n*14|//! and the new brain must re-attach to the output stream **gaplessly and\n 15|//! exactly-once** (Spike #1 made real). Two mechanisms:\n 16|//!\n 17|//! - **Sequenced, buffered output.** Each session's output is appended to an\n...\n 24|//!   live. The brain dedups by `seq`. Output produced *while no brain was\n*25|//!   attached* sits in the ring and is replayed on the next subscribe — so a\n 26|//!   brain restart loses nothing.\n 27|//!\n 28|//! Sessions are therefore **broker-owned and outlive any one connection**: a\n...\n 44|\n*45|use spt_net::net::attach::AttachIntent;\n 46|use spt_net::net::endpoint::EndpointAddr;\n 47|use spt_store::perch::{resolve_perch_path, ParentHint};\n 48|\n...\n 93|/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n*94|/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n*95|/// owns the replay batch), so a viewer attaching to a busy session is not\n 96|/// spuriously evicted.\n 97|const VIEWER_CHANNEL_DEPTH: usize = 256;\n 98|\n*99|/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n*100|/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\n 101|const MAX_VIEWERS: usize = 32;\n 102|\n 103|/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n...\n 128|    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n*129|    /// the serving brain skips to live instead of tearing the attach down.\n 130|    evicted: Arc<AtomicBool>,\n 131|    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n 132|    /// a socket write fails).\n...\n 141|/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n*142|/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n 143|/// never trips the deadline.\n 144|const CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n 145|\n 146|/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n*147|/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n 148|/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n 149|/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n 150|/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n...\n 156|/// How long a controller's writer thread may sit BLOCKED inside a single socket\n*157|/// write — the brain subscriber suspended / black-holed — before a take/reattach\n 158|/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n 159|/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n 160|/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n...\n 165|/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n*166|/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n 167|/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n 168|/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n 169|/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n*170|/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 171|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 172|const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 173|\n...\n 175|/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n*176|/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 177|/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 178|/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n 179|/// ONLY — never relaxes an assertion.\n...\n 241|\n*242|/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n 243|/// I/O-under-log-lock) or DEFERS the stamp to the off-lock convergence (the\n 244|/// multi-session reap closure that forbids I/O under the shared sessions lock).\n 245|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n...\n 353|// The typed idle-inject leg ate HEAD bytes nondeterministically: bytes typed into a\n*354|// PTY whose input reader is not yet attached/settled are swallowed mid-stream (the\n 355|// pre-settle race — 1854B once landed intact, ~1150B usually swallowed = a timing\n 356|// class, not a size cap). A live-SENT inject leaves NO spool copy, so a swallow is\n 357|// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n...\n 373|/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n*374|/// producing output — the POSITIVE signal that the input reader is attached.\n 375|const INJECT_SETTLE_PROBE: &[u8] = b\"\\x1b[6n\";\n 376|\n 377|/// The effective settle-gate deadline; the int gate tunes it via\n...\n 493|/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].\n*494|struct ControllerSink {\n 495|    /// The bounded live-frame queue to the controller's writer thread.\n 496|    tx: SyncSender<CtrlMsg>,\n 497|    /// The controller's IPC send half — kept for identity matching on detach and\n...\n 534|/// Output fans out to ONE interactive `controller` (the authoritative,\n*535|/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each\n 536|/// an isolated bounded queue + writer thread, evicted on overflow). Every\n 537|/// controller send and viewer `try_send` happens while the enclosing\n 538|/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\n...\n 546|    /// controller's identity (`Some(node)` remote / `None` local) lives in\n*547|    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe\n 548|    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a\n 549|    /// DIFFERENT remote's `Take` displaces.\n### attach.rs\n*1|//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n 2|//! onto a local session for an operator on another node.\n 3|//!\n 4|//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n...\n 20|//! eats the operator's `Input` records (both sides starve; found the hard way\n*21|//! in `tests/attach.rs`). Idempotency holds without it because each direction\n 22|//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n 23|//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n 24|//! session-log `seq` (the render cursor). A post-restart re-serve may\n...\n 38|//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n*39|//! A restarted target brain re-attaches the session from its output cursor,\n 40|//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n 41|//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42|//! `Output.seq`.\n...\n 46|\n*47|use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n 48|use spt_net::net::ndjson;\n 49|\n 50|use crate::access::{access_check, AccessDecision, InboundClass};\n...\n 54|\n*55|/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n*56|/// served viewport is a **driver** — attaching wakes the endpoint, the\n 57|/// viewport ending detaches it (active → dormant; the transition echo is\n 58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n 59|/// [`mark_driven_by`]: a session served without a perch has no resting\n...\n 68|pub enum AttachServeOutcome {\n*69|    /// The access gate refused the requesting origin, OR a `Control` attach hit\n 70|    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n 71|    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n 72|    /// busy guidance client-side from the endpoint's `driven_by`.\n...\n 135|\n*136|/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\n 137|/// b4 drop-don't-block output-gap instead of fataling.\n 138|///\n 139|/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\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 194|/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n*195|/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n 196|/// its own session table, so a remote operator — which cannot know our local\n 197|/// session id — names the endpoint and we map it here, never trusting a wire\n 198|/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*199|/// (a stale registry row routed us an attach for something gone → the caller\n 200|/// refuses cleanly, D6).\n 201|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 202|pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n...\n 211|\n*212|/// Serve one attach stream against this brain's broker, blocking until the\n 213|/// session exits, the operator detaches, or the gate refuses.\n 214|///\n 215|/// `origin_node` MUST be the handshake-proven remote id from the broker's\n...\n 217|/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n*218|/// is being attached, for the gate. `net_from_seq` is this brain's resume\n*219|/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n 220|/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n 221|/// journal, and re-pumped output dedups at the operator's render cursor).\n 222|///\n...\n 227|/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n*228|/// round-trip inside the event loop would swallow interleaved attach records\n 229|/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n 230|/// the wire `session_id`.\n 231|// [impl->REQ-INST-8]\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 253|    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n*254|    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n 255|    // two directions on independent single-threaded handlers cannot block each\n 256|    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n 257|    // a second conn is correct. Order is preserved: this one serve thread reads\n...\n 265|    let mut decoder = AttachDecoder::new();\n*266|    let mut attached = false;\n 267|    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n 268|    // the PTY) independent of the broker's later role reply (no IPC-ordering\n 269|    // race on the input gate).\n...\n 276|    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n*277|    // The attached session — captured from the Request so the terminal paths can\n 278|    // explicitly release this worker's role (clearing driven_by / viewer_count\n 279|    // promptly, not only on connection drop).\n*280|    let mut attached_session: Option<u64> = None;\n 281|    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n 282|    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n 283|    // intermittent live bursts, never a CPU spin.\n\n## spt/src/\n### rc.rs\n*1|//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\n 2|//! local terminal to a broker-held PTY session.\n 3|//!\n*4|//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n*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 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*33|use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n 34|\n 35|/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n 36|/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n...\n 209|\n*210|/// Whether the controller reserved-row identity marker is active for this attach:\n 211|/// only for a controller (a viewer's viewport is read-only) AND only while the\n 212|/// marker is enabled (bug #14 ships it off).\n 213|// [impl->REQ-RC-IDMARKER-DISABLE]\n...\n 221|/// scroll region reserving that row (DECSTBM). It tells the operator WHICH endpoint\n*222|/// on WHICH node they are driving, so a multi-attach operator never types into the\n 223|/// wrong session. Pure byte-emit (no I/O) so the exact VT shapes unit-test\n 224|/// directly; the pump writes the bytes. Tracks the FULL terminal `rows` (it paints\n 225|/// row 1 on the real terminal) while the PTY is told it has `rows - 1`.\n...\n 301|/// `{subnet} : {endpoint_id} @ {node}` — the endpoint's HOME subnet (`local` when\n*302|/// unset), the attach target, and THIS machine's hostname label. An empty/absent\n 303|/// node label omits the ` @ {node}` tail rather than rendering a lie. Resolved ONCE\n*304|/// at attach. Pure over its inputs so the format unit-tests without the store.\n 305|// [impl->REQ-RC-IDENTITY]\n 306|fn identity_line(subnet: &str, endpoint_id: &str, node: Option<&str>) -> String {\n 307|    match node.map(str::trim).filter(|n| !n.is_empty()) {\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 870|/// A thin, reusable broker connection for polling \"is there a live session for\n*871|/// this endpoint?\" — the W4 attach-on-session-exists gate\n 872|/// (REQ-ENDPOINT-UNBOUND-ATTACH). One broker connect, then [`has_session`] is\n 873|/// queried across the bounded await loop (so the await never re-dials per\n 874|/// iteration). Keys on the broker sessions map (ADR-0025 W3a) — independent of\n*875|/// perch status, so an UNBOUND skeleton's live session reports attachable.\n 876|///\n 877|/// [`has_session`]: SessionProbe::has_session\n 878|pub(crate) struct SessionProbe {\n...\n 1081|// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n*1082|pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n*1083|    run_attach_inner(endpoint_id, intent, false)\n 1084|}\n 1085|\n*1086|/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n 1087|/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n 1088|/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n 1089|/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/attach.rs, crates/spt/src/rc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":60,"fileCount":3,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/attach.rs","crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/attach.rs","count":20},{"path":"crates/spt/src/rc.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/src/\n### broker.rs\n  13│//! crash). The broker — and every hosted child — must survive that untouched,\n *14│//! and the new brain must re-attach to the output stream **gaplessly and\n  15│//! exactly-once** (Spike #1 made real). Two mechanisms:\n  16│//!\n  17│//! - **Sequenced, buffered output.** Each session's output is appended to an\n    │...\n  24│//!   live. The brain dedups by `seq`. Output produced *while no brain was\n *25│//!   attached* sits in the ring and is replayed on the next subscribe — so a\n  26│//!   brain restart loses nothing.\n  27│//!\n  28│//! Sessions are therefore **broker-owned and outlive any one connection**: a\n    │...\n  44│\n *45│use spt_net::net::attach::AttachIntent;\n  46│use spt_net::net::endpoint::EndpointAddr;\n  47│use spt_store::perch::{resolve_perch_path, ParentHint};\n  48│\n    │...\n  93│/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n *94│/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n *95│/// owns the replay batch), so a viewer attaching to a busy session is not\n  96│/// spuriously evicted.\n  97│const VIEWER_CHANNEL_DEPTH: usize = 256;\n  98│\n *99│/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n*100│/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\n 101│const MAX_VIEWERS: usize = 32;\n 102│\n 103│/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n    │...\n 128│    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n*129│    /// the serving brain skips to live instead of tearing the attach down.\n 130│    evicted: Arc<AtomicBool>,\n 131│    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n 132│    /// a socket write fails).\n    │...\n 141│/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n*142│/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n 143│/// never trips the deadline.\n 144│const CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n 145│\n 146│/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n*147│/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n 148│/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n 149│/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n 150│/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n    │...\n 156│/// How long a controller's writer thread may sit BLOCKED inside a single socket\n*157│/// write — the brain subscriber suspended / black-holed — before a take/reattach\n 158│/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n 159│/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n 160│/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n    │...\n 165│/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n*166│/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n 167│/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n 168│/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n 169│/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n*170│/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 171│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 172│const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 173│\n    │...\n 175│/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n*176│/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 177│/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 178│/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n 179│/// ONLY — never relaxes an assertion.\n    │...\n 241│\n*242│/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n 243│/// I/O-under-log-lock) or DEFERS the stamp to the off-lock convergence (the\n 244│/// multi-session reap closure that forbids I/O under the shared sessions lock).\n 245│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    │...\n 353│// The typed idle-inject leg ate HEAD bytes nondeterministically: bytes typed into a\n*354│// PTY whose input reader is not yet attached/settled are swallowed mid-stream (the\n 355│// pre-settle race — 1854B once landed intact, ~1150B usually swallowed = a timing\n 356│// class, not a size cap). A live-SENT inject leaves NO spool copy, so a swallow is\n 357│// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n    │...\n 373│/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n*374│/// producing output — the POSITIVE signal that the input reader is attached.\n 375│const INJECT_SETTLE_PROBE: &[u8] = b\"\\x1b[6n\";\n 376│\n 377│/// The effective settle-gate deadline; the int gate tunes it via\n    │...\n 493│/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].\n*494│struct ControllerSink {\n 495│    /// The bounded live-frame queue to the controller's writer thread.\n 496│    tx: SyncSender<CtrlMsg>,\n 497│    /// The controller's IPC send half — kept for identity matching on detach and\n    │...\n 534│/// Output fans out to ONE interactive `controller` (the authoritative,\n*535│/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each\n 536│/// an isolated bounded queue + writer thread, evicted on overflow). Every\n 537│/// controller send and viewer `try_send` happens while the enclosing\n 538│/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\n    │...\n 546│    /// controller's identity (`Some(node)` remote / `None` local) lives in\n*547│    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe\n 548│    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a\n 549│    /// DIFFERENT remote's `Take` displaces.\n### attach.rs\n  *1│//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n   2│//! onto a local session for an operator on another node.\n   3│//!\n   4│//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n    │...\n  20│//! eats the operator's `Input` records (both sides starve; found the hard way\n *21│//! in `tests/attach.rs`). Idempotency holds without it because each direction\n  22│//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n  23│//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n  24│//! session-log `seq` (the render cursor). A post-restart re-serve may\n    │...\n  38│//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n *39│//! A restarted target brain re-attaches the session from its output cursor,\n  40│//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n  41│//! a restarted operator brain resubscribes and dedups rendered chunks by\n  42│//! `Output.seq`.\n    │...\n  46│\n *47│use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n  48│use spt_net::net::ndjson;\n  49│\n  50│use crate::access::{access_check, AccessDecision, InboundClass};\n    │...\n  54│\n *55│/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n *56│/// served viewport is a **driver** — attaching wakes the endpoint, the\n  57│/// viewport ending detaches it (active → dormant; the transition echo is\n  58│/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n  59│/// [`mark_driven_by`]: a session served without a perch has no resting\n    │...\n  68│pub enum AttachServeOutcome {\n *69│    /// The access gate refused the requesting origin, OR a `Control` attach hit\n  70│    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n  71│    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n  72│    /// busy guidance client-side from the endpoint's `driven_by`.\n    │...\n 135│\n*136│/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\n 137│/// b4 drop-don't-block output-gap instead of fataling.\n 138│///\n 139│/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\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 194│/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n*195│/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n 196│/// its own session table, so a remote operator — which cannot know our local\n 197│/// session id — names the endpoint and we map it here, never trusting a wire\n 198│/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*199│/// (a stale registry row routed us an attach for something gone → the caller\n 200│/// refuses cleanly, D6).\n 201│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 202│pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    │...\n 211│\n*212│/// Serve one attach stream against this brain's broker, blocking until the\n 213│/// session exits, the operator detaches, or the gate refuses.\n 214│///\n 215│/// `origin_node` MUST be the handshake-proven remote id from the broker's\n    │...\n 217│/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n*218│/// is being attached, for the gate. `net_from_seq` is this brain's resume\n*219│/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n 220│/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n 221│/// journal, and re-pumped output dedups at the operator's render cursor).\n 222│///\n    │...\n 227│/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n*228│/// round-trip inside the event loop would swallow interleaved attach records\n 229│/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n 230│/// the wire `session_id`.\n 231│// [impl->REQ-INST-8]\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 253│    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n*254│    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n 255│    // two directions on independent single-threaded handlers cannot block each\n 256│    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n 257│    // a second conn is correct. Order is preserved: this one serve thread reads\n    │...\n 265│    let mut decoder = AttachDecoder::new();\n*266│    let mut attached = false;\n 267│    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n 268│    // the PTY) independent of the broker's later role reply (no IPC-ordering\n 269│    // race on the input gate).\n    │...\n 276│    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n*277│    // The attached session — captured from the Request so the terminal paths can\n 278│    // explicitly release this worker's role (clearing driven_by / viewer_count\n 279│    // promptly, not only on connection drop).\n*280│    let mut attached_session: Option<u64> = None;\n 281│    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n 282│    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n 283│    // intermittent live bursts, never a CPU spin.\n\n## spt/src/\n### rc.rs\n   *1│//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\n    2│//! local terminal to a broker-held PTY session.\n    3│//!\n   *4│//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n   *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   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  *33│use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n   34│\n   35│/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n   36│/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n     │...\n  209│\n *210│/// Whether the controller reserved-row identity marker is active for this attach:\n  211│/// only for a controller (a viewer's viewport is read-only) AND only while the\n  212│/// marker is enabled (bug #14 ships it off).\n  213│// [impl->REQ-RC-IDMARKER-DISABLE]\n     │...\n  221│/// scroll region reserving that row (DECSTBM). It tells the operator WHICH endpoint\n *222│/// on WHICH node they are driving, so a multi-attach operator never types into the\n  223│/// wrong session. Pure byte-emit (no I/O) so the exact VT shapes unit-test\n  224│/// directly; the pump writes the bytes. Tracks the FULL terminal `rows` (it paints\n  225│/// row 1 on the real terminal) while the PTY is told it has `rows - 1`.\n     │...\n  301│/// `{subnet} : {endpoint_id} @ {node}` — the endpoint's HOME subnet (`local` when\n *302│/// unset), the attach target, and THIS machine's hostname label. An empty/absent\n  303│/// node label omits the ` @ {node}` tail rather than rendering a lie. Resolved ONCE\n *304│/// at attach. Pure over its inputs so the format unit-tests without the store.\n  305│// [impl->REQ-RC-IDENTITY]\n  306│fn identity_line(subnet: &str, endpoint_id: &str, node: Option<&str>) -> String {\n  307│    match node.map(str::trim).filter(|n| !n.is_empty()) {\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  870│/// A thin, reusable broker connection for polling \"is there a live session for\n *871│/// this endpoint?\" — the W4 attach-on-session-exists gate\n  872│/// (REQ-ENDPOINT-UNBOUND-ATTACH). One broker connect, then [`has_session`] is\n  873│/// queried across the bounded await loop (so the await never re-dials per\n  874│/// iteration). Keys on the broker sessions map (ADR-0025 W3a) — independent of\n *875│/// perch status, so an UNBOUND skeleton's live session reports attachable.\n  876│///\n  877│/// [`has_session`]: SessionProbe::has_session\n  878│pub(crate) struct SessionProbe {\n     │...\n 1081│// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n*1082│pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n*1083│    run_attach_inner(endpoint_id, intent, false)\n 1084│}\n 1085│\n*1086│/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n 1087│/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n 1088│/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n 1089│/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2"},"isError":false,"timestamp":1784195766614}}
{"type":"message","id":"1fa51143","parentId":"23ad99b5","timestamp":"2026-07-16T09:56:06.624Z","message":{"role":"toolResult","toolCallId":"call_g4Niy1vL33Wq8Pgs417gIWV0|fc_0cc7a0c9a797f232016a58aab65e54819aa95414e70181b3bf","toolName":"read","content":[{"type":"text","text":"//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n//! (\"nothing in the daemon invokes this apply after the user acks the consent\n//! notif\").\n//!\n//! The ack IS the verb the consent notif names: its body says \"confirm to\n//! apply (`spt update apply`) or dismiss\" (ADR-0007 — the harness's native\n//! confirm affordance answers by running the command; dismiss latches the\n//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n//! still works, the user changed their mind).\n//!\n//! [`apply_staged`] drives the whole chain against what the peer pump left\n//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):\n//!\n//! 1. **Re-verify at rest** (`plan_verified` under the production\n//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n//!    or expired artifact refuses here. The monotonic-version floor was\n//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n//! 2. **Classify** against the running broker's ABI. Only\n//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n//!    orchestration is post-M5; ADR-0004 §A).\n//! 3. **Swap the binary**: the current executable steps aside\n//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n//!    OSes) and the verified artifact lands at the executable path\n//!    atomically. A failed landing rolls the original back — never a\n//!    half-swapped seat.\n//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],\n//!    ADR-0018 D3-3): the broker signals its supervisor to cycle the brain\n//!    child onto the just-swapped binary with `start-reason=update`. No hosted\n//!    endpoint terminates or suspends (REQ-UPD-3, absolute for this class — the\n//!    broker holds them all across the cycle). This replaces the old in-process\n//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n//!    subscriber while the live brain kept running the OLD code.\n//!\n//! New code runs **immediately** — the supervised respawn lands on the new\n//! executable, no manual bounce, no logon. Readiness-gated promotion +\n//! auto-rollback on a brain that fails to come up is D6.\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\n\nuse std::io;\nuse std::path::Path;\nuse std::time::Duration;\n\nuse crate::brain::{now_ms, Brain};\nuse crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\nuse crate::release::{current_platform, VerifyPolicy};\nuse crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n\n/// What [`apply_staged`] did — loud + typed, never silent.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum ApplyStagedOutcome {\n    /// Nothing staged (or a torn stage that never offers) — idempotent no-op.\n    NoUpdate,\n    /// The staged version is already the running/applied image — an idempotent\n    /// exit-0 no-op (F-025). Short-circuits BEFORE the binary-aside rename, whose\n    /// re-run on an already-swapped exe is the 'Access is denied (os error 5)'\n    /// this fixes. `version`/`product_version` name what is already installed.\n    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n    AlreadyApplied {\n        version: u64,\n        product_version: String,\n    },\n    /// The at-rest re-verification refused (tamper / expiry / key trust) —\n    /// nothing swapped, the reason says why.\n    Rejected(String),\n    /// Verified but not brain-only — the M5 apply surface refuses the\n    /// broker-touching classes typed; the artifact stays staged.\n    RefusedClass(UpdateClass),\n    /// Refused: this version was auto-rolled-back on this node (a prior trial\n    /// failed readiness) and is **quarantined** — never re-applied until a newer\n    /// version stages or an operator forces it (the deferred escape hatch). The\n    /// daemon stays healthy on the rolled-back binary (ADR-0018 Q7, D6-2).\n    Quarantined { version: u64 },\n    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n    /// provisional immediately (it cannot observe the asynchronous verdict — the\n    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n    /// be the optimistic enlyzeam write D6 exists to kill. `version` is the\n    /// monotonic counter; `product_version` the human-facing semver (empty for\n    /// pre-v0.3.2 metadata, whence the counter-based fallback message).\n    // [impl->REQ-UPD-6]\n    // [impl->REQ-UPD-8]\n    AppliedPending {\n        version: u64,\n        product_version: String,\n    },\n    /// Swapped the binary on disk with NO running daemon touched — a\n    /// **daemonless** apply (REQ-UPDATE-ONE-SHOT-FINISH). No broker was booted to\n    /// hand off to (the old-broker-boot-pre-swap wart) and no brain was cycled;\n    /// the `AppliedPending` trial is recorded so the NEXT daemon start boots the\n    /// new bytes and promotes it on the fresh brain's `ready`. `version` is the\n    /// monotonic counter; `product_version` the human semver (empty pre-v0.3.2).\n    // [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n    AppliedDaemonless {\n        version: u64,\n        product_version: String,\n    },\n}\n\n/// Either an early terminal apply outcome (nothing swaps) or the go-ahead to\n/// swap, carrying the verified bytes. The shared verify/classify preamble both\n/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on\n/// whether a live broker is handed off to.\nenum Prepared {\n    /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /\n    /// Quarantined / Rejected / RefusedClass) — return it verbatim.\n    Done(ApplyStagedOutcome),\n    /// Verified brain-only bytes ready to land at the exe path.\n    Proceed {\n        version: u64,\n        product_version: String,\n…\n    },\n}\n…\n/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n/// whether a running brain is cycled afterwards.\n// [impl->REQ-UPD-6]\nfn swap_and_record(\n    cache: &ReleaseCache,\n    exe_path: &Path,\n    version: u64,\n    artifact: &[u8],\n) -> Result<(), String> {\n    // Swap the binary: step aside, land the artifact, roll back on failure.\n    let aside = swap_aside_path(exe_path, version);\n    let _ = std::fs::remove_file(&aside); // stale prior apply of this version\n    std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;\n    if let Err(e) = land_artifact(exe_path, artifact) {\n        let _ = std::fs::rename(&aside, exe_path); // roll back — never half-swapped\n        return Err(format!(\"land new binary (rolled back): {e}\"));\n    }\n    // The binary IS swapped from here on — but DO NOT claim `applied` yet\n    // (ADR-0018 Q7, restoration D6-1): the new brain has not booted, let alone\n    // reached `ready`. Writing `applied` here is the optimism the design indicts\n    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n    // carrying the last-known-good `rollback_binary` (the aside we just created)\n    // so the broker's supervisor, a different process from this CLI, can promote\n    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n    // [impl->REQ-UPD-6]\n    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n        version,\n        rollback_binary: aside.to_string_lossy().into_owned(),\n        candidate_started_ms: now_ms(),\n        // The version actually running before this swap — what a rollback returns\n        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n        // the quarantined version, not the running one. `None` when nothing was\n        // ever recorded applied (a first-ever apply) → the supervisor falls back\n        // to N-1, the only honest guess then.\n        prior_version: cache.applied_version(),\n    });\n    let _ = cache.record_last_outcome(\"apply-pending\");\n    Ok(())\n}\n\n/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n/// the canonical broker socket the brain handoff runs against. For a stopped box,\n/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\npub fn apply_staged(\n    cache: &ReleaseCache,\n    release_keys: &Path,\n    exe_path: &Path,\n    broker_name: &str,\n) -> Result<ApplyStagedOutcome, String> {\n    let (version, product_version, artifact) = match prepare_apply(cache, release_keys)? {\n        Prepared::Done(outcome) => return Ok(outcome),\n        Prepared::Proceed {\n            version,\n            product_version,\n            artifact,\n        } => (version, product_version, artifact),\n    };\n\n    // Connect to the broker BEFORE the binary moves — a dead broker fails the\n    // apply with the seat untouched. The connection is the channel the planned\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/// the swapped bytes run when the daemon is NEXT started (either by the operator,\n/// or by `update apply --finish` which restarts it in the same command, riding\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so hosted endpoints come back). The\n/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n/// the handoff path's does. Runs the identical verify/classify gate as\n/// [`apply_staged`], so every fail-closed refusal is preserved.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n// [impl->REQ-UPD-4]\npub fn apply_staged_daemonless(\n    cache: &ReleaseCache,\n    release_keys: &Path,\n    exe_path: &Path,\n) -> Result<ApplyStagedOutcome, String> {\n    let (version, product_version, artifact) = match prepare_apply(cache, release_keys)? {\n        Prepared::Done(outcome) => return Ok(outcome),\n        Prepared::Proceed {\n            version,\n            product_version,\n            artifact,\n        } => (version, product_version, artifact),\n    };\n    swap_and_record(cache, exe_path, version, &artifact)?;\n    Ok(ApplyStagedOutcome::AppliedDaemonless {\n        version,\n        product_version,\n    })\n}\n\n/// Already-applied classifier (F-025): whether the staged `staged_version` is\n/// already the running/applied image, so `apply_staged` must short-circuit BEFORE\n…\n    fn brain_restart_verb_raises_the_supervisor_signal() {\n…\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        #[cfg(windows)]\n        let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n        SpawnReq {\n            program,\n            args,\n…\n        }\n    }\n\n    // [unit->REQ-UPD-4] the ack-driven apply end to end: a staged + verified\n    // brain-only release swaps the binary (artifact lands at the exe path,\n    // the original steps aside) and triggers the brain handoff with the\n    // hosted endpoint untouched — the production caller the M4-D9 residual\n    // promised. [unit->REQ-UPD-3]\n    #[test]\n    fn apply_staged_swaps_binary_and_hands_off_without_touching_the_endpoint() {\n        let dir = tempfile::tempdir().unwrap();\n        let name = unique_name();\n        let broker = served_broker(&name);\n\n        // A hosted endpoint that must survive the apply.\n        let mut seat = cold_connect_retry(&name);\n        let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");\n        let child_pid = broker.session_pid(sid);\n        drop(seat); // the seat brain detaches; the broker keeps the child\n\n        let exe = dir.path().join(\"spt-binary\");\n        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n\n        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"apply ok\");\n        assert!(\n            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n            \"got {out:?}\"\n        );\n        assert_eq!(\n            std::fs::read(&exe).unwrap(),\n            b\"NEW BINARY BYTES\",\n            \"artifact landed\"\n        );\n        assert_eq!(\n            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n            b\"OLD BINARY\",\n            \"the outgoing binary stepped aside\"\n        );\n        // REQ-UPD-3: the hosted child never noticed.\n        assert_eq!(\n            broker.session_count(),\n            1,\n            \"exactly one session across the apply\"\n        );\n        assert_eq!(broker.session_pid(sid), child_pid, \"child pid unchanged\");\n    }\n\n    // [unit->REQ-UPDATE-ONE-SHOT-FINISH] the daemonless apply: with NO broker\n    // running AT ALL (none bound, none served — the stopped-box shape the wart\n    // used to boot the old broker for), apply_staged_daemonless still swaps the\n    // binary + records the open trial and returns AppliedDaemonless. RED without\n    // the daemonless path: apply_staged would block on connect_retry against a\n    // dead socket (300×10ms) and then fail — never landing the bytes. Proves the\n    // swap no longer depends on a live broker to hand off to.\n    #[test]\n    fn apply_staged_daemonless_swaps_binary_with_no_broker() {\n        let dir = tempfile::tempdir().unwrap();\n        let exe = dir.path().join(\"spt-binary\");\n        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n\n        // No served_broker — deliberately. A daemonless apply must not need one.\n        let out = apply_staged_daemonless(&cache, &keys, &exe).expect(\"apply ok\");\n        assert!(\n            matches!(out, ApplyStagedOutcome::AppliedDaemonless { version: 7, .. }),\n            \"got {out:?}\"\n        );\n        assert_eq!(\n            std::fs::read(&exe).unwrap(),\n            b\"NEW BINARY BYTES\",\n            \"artifact landed without any broker handoff\"\n        );\n        assert_eq!(\n            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n            b\"OLD BINARY\",\n            \"the outgoing binary stepped aside\"\n        );\n…\n    }"}],"details":{"displayContent":{"text":"//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n//! (\"nothing in the daemon invokes this apply after the user acks the consent\n//! notif\").\n//!\n//! The ack IS the verb the consent notif names: its body says \"confirm to\n//! apply (`spt update apply`) or dismiss\" (ADR-0007 — the harness's native\n//! confirm affordance answers by running the command; dismiss latches the\n//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n//! still works, the user changed their mind).\n//!\n//! [`apply_staged`] drives the whole chain against what the peer pump left\n//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):\n//!\n//! 1. **Re-verify at rest** (`plan_verified` under the production\n//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n//!    or expired artifact refuses here. The monotonic-version floor was\n//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n//! 2. **Classify** against the running broker's ABI. Only\n//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n//!    orchestration is post-M5; ADR-0004 §A).\n//! 3. **Swap the binary**: the current executable steps aside\n//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n//!    OSes) and the verified artifact lands at the executable path\n//!    atomically. A failed landing rolls the original back — never a\n//!    half-swapped seat.\n//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],\n//!    ADR-0018 D3-3): the broker signals its supervisor to cycle the brain\n//!    child onto the just-swapped binary with `start-reason=update`. No hosted\n//!    endpoint terminates or suspends (REQ-UPD-3, absolute for this class — the\n//!    broker holds them all across the cycle). This replaces the old in-process\n//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n//!    subscriber while the live brain kept running the OLD code.\n//!\n//! New code runs **immediately** — the supervised respawn lands on the new\n//! executable, no manual bounce, no logon. Readiness-gated promotion +\n//! auto-rollback on a brain that fails to come up is D6.\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\n\nuse std::io;\nuse std::path::Path;\nuse std::time::Duration;\n\nuse crate::brain::{now_ms, Brain};\nuse crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\nuse crate::release::{current_platform, VerifyPolicy};\nuse crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n\n/// What [`apply_staged`] did — loud + typed, never silent.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum ApplyStagedOutcome {\n    /// Nothing staged (or a torn stage that never offers) — idempotent no-op.\n    NoUpdate,\n    /// The staged version is already the running/applied image — an idempotent\n    /// exit-0 no-op (F-025). Short-circuits BEFORE the binary-aside rename, whose\n    /// re-run on an already-swapped exe is the 'Access is denied (os error 5)'\n    /// this fixes. `version`/`product_version` name what is already installed.\n    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n    AlreadyApplied {\n        version: u64,\n        product_version: String,\n    },\n    /// The at-rest re-verification refused (tamper / expiry / key trust) —\n    /// nothing swapped, the reason says why.\n    Rejected(String),\n    /// Verified but not brain-only — the M5 apply surface refuses the\n    /// broker-touching classes typed; the artifact stays staged.\n    RefusedClass(UpdateClass),\n    /// Refused: this version was auto-rolled-back on this node (a prior trial\n    /// failed readiness) and is **quarantined** — never re-applied until a newer\n    /// version stages or an operator forces it (the deferred escape hatch). The\n    /// daemon stays healthy on the rolled-back binary (ADR-0018 Q7, D6-2).\n    Quarantined { version: u64 },\n    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n    /// provisional immediately (it cannot observe the asynchronous verdict — the\n    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n    /// be the optimistic enlyzeam write D6 exists to kill. `version` is the\n    /// monotonic counter; `product_version` the human-facing semver (empty for\n    /// pre-v0.3.2 metadata, whence the counter-based fallback message).\n    // [impl->REQ-UPD-6]\n    // [impl->REQ-UPD-8]\n    AppliedPending {\n        version: u64,\n        product_version: String,\n    },\n    /// Swapped the binary on disk with NO running daemon touched — a\n    /// **daemonless** apply (REQ-UPDATE-ONE-SHOT-FINISH). No broker was booted to\n    /// hand off to (the old-broker-boot-pre-swap wart) and no brain was cycled;\n    /// the `AppliedPending` trial is recorded so the NEXT daemon start boots the\n    /// new bytes and promotes it on the fresh brain's `ready`. `version` is the\n    /// monotonic counter; `product_version` the human semver (empty pre-v0.3.2).\n    // [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n    AppliedDaemonless {\n        version: u64,\n        product_version: String,\n    },\n}\n\n/// Either an early terminal apply outcome (nothing swaps) or the go-ahead to\n/// swap, carrying the verified bytes. The shared verify/classify preamble both\n/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on\n/// whether a live broker is handed off to.\nenum Prepared {\n    /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /\n    /// Quarantined / Rejected / RefusedClass) — return it verbatim.\n    Done(ApplyStagedOutcome),\n    /// Verified brain-only bytes ready to land at the exe path.\n    Proceed {\n        version: u64,\n        product_version: String,\n…\n    },\n}\n…\n/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n/// whether a running brain is cycled afterwards.\n// [impl->REQ-UPD-6]\nfn swap_and_record(\n    cache: &ReleaseCache,\n    exe_path: &Path,\n    version: u64,\n    artifact: &[u8],\n) -> Result<(), String> {\n    // Swap the binary: step aside, land the artifact, roll back on failure.\n    let aside = swap_aside_path(exe_path, version);\n    let _ = std::fs::remove_file(&aside); // stale prior apply of this version\n    std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;\n    if let Err(e) = land_artifact(exe_path, artifact) {\n        let _ = std::fs::rename(&aside, exe_path); // roll back — never half-swapped\n        return Err(format!(\"land new binary (rolled back): {e}\"));\n    }\n    // The binary IS swapped from here on — but DO NOT claim `applied` yet\n    // (ADR-0018 Q7, restoration D6-1): the new brain has not booted, let alone\n    // reached `ready`. Writing `applied` here is the optimism the design indicts\n    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n    // carrying the last-known-good `rollback_binary` (the aside we just created)\n    // so the broker's supervisor, a different process from this CLI, can promote\n    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n    // [impl->REQ-UPD-6]\n    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n        version,\n        rollback_binary: aside.to_string_lossy().into_owned(),\n        candidate_started_ms: now_ms(),\n        // The version actually running before this swap — what a rollback returns\n        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n        // the quarantined version, not the running one. `None` when nothing was\n        // ever recorded applied (a first-ever apply) → the supervisor falls back\n        // to N-1, the only honest guess then.\n        prior_version: cache.applied_version(),\n    });\n    let _ = cache.record_last_outcome(\"apply-pending\");\n    Ok(())\n}\n\n/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n/// the canonical broker socket the brain handoff runs against. For a stopped box,\n/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\npub fn apply_staged(\n    cache: &ReleaseCache,\n    release_keys: &Path,\n    exe_path: &Path,\n    broker_name: &str,\n) -> Result<ApplyStagedOutcome, String> {\n    let (version, product_version, artifact) = match prepare_apply(cache, release_keys)? {\n        Prepared::Done(outcome) => return Ok(outcome),\n        Prepared::Proceed {\n            version,\n            product_version,\n            artifact,\n        } => (version, product_version, artifact),\n    };\n\n    // Connect to the broker BEFORE the binary moves — a dead broker fails the\n    // apply with the seat untouched. The connection is the channel the planned\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/// the swapped bytes run when the daemon is NEXT started (either by the operator,\n/// or by `update apply --finish` which restarts it in the same command, riding\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so hosted endpoints come back). The\n/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n/// the handoff path's does. Runs the identical verify/classify gate as\n/// [`apply_staged`], so every fail-closed refusal is preserved.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n// [impl->REQ-UPD-4]\npub fn apply_staged_daemonless(\n    cache: &ReleaseCache,\n    release_keys: &Path,\n    exe_path: &Path,\n) -> Result<ApplyStagedOutcome, String> {\n    let (version, product_version, artifact) = match prepare_apply(cache, release_keys)? {\n        Prepared::Done(outcome) => return Ok(outcome),\n        Prepared::Proceed {\n            version,\n            product_version,\n            artifact,\n        } => (version, product_version, artifact),\n    };\n    swap_and_record(cache, exe_path, version, &artifact)?;\n    Ok(ApplyStagedOutcome::AppliedDaemonless {\n        version,\n        product_version,\n    })\n}\n\n/// Already-applied classifier (F-025): whether the staged `staged_version` is\n/// already the running/applied image, so `apply_staged` must short-circuit BEFORE\n…\n    fn brain_restart_verb_raises_the_supervisor_signal() {\n…\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        #[cfg(windows)]\n        let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n        SpawnReq {\n            program,\n            args,\n…\n        }\n    }\n\n    // [unit->REQ-UPD-4] the ack-driven apply end to end: a staged + verified\n    // brain-only release swaps the binary (artifact lands at the exe path,\n    // the original steps aside) and triggers the brain handoff with the\n    // hosted endpoint untouched — the production caller the M4-D9 residual\n    // promised. [unit->REQ-UPD-3]\n    #[test]\n    fn apply_staged_swaps_binary_and_hands_off_without_touching_the_endpoint() {\n        let dir = tempfile::tempdir().unwrap();\n        let name = unique_name();\n        let broker = served_broker(&name);\n\n        // A hosted endpoint that must survive the apply.\n        let mut seat = cold_connect_retry(&name);\n        let sid = seat.spawn_session(echo_spawn_req()).expect(\"spawn\");\n        let child_pid = broker.session_pid(sid);\n        drop(seat); // the seat brain detaches; the broker keeps the child\n\n        let exe = dir.path().join(\"spt-binary\");\n        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n\n        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"apply ok\");\n        assert!(\n            matches!(out, ApplyStagedOutcome::AppliedPending { version: 7, .. }),\n            \"got {out:?}\"\n        );\n        assert_eq!(\n            std::fs::read(&exe).unwrap(),\n            b\"NEW BINARY BYTES\",\n            \"artifact landed\"\n        );\n        assert_eq!(\n            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n            b\"OLD BINARY\",\n            \"the outgoing binary stepped aside\"\n        );\n        // REQ-UPD-3: the hosted child never noticed.\n        assert_eq!(\n            broker.session_count(),\n            1,\n            \"exactly one session across the apply\"\n        );\n        assert_eq!(broker.session_pid(sid), child_pid, \"child pid unchanged\");\n    }\n\n    // [unit->REQ-UPDATE-ONE-SHOT-FINISH] the daemonless apply: with NO broker\n    // running AT ALL (none bound, none served — the stopped-box shape the wart\n    // used to boot the old broker for), apply_staged_daemonless still swaps the\n    // binary + records the open trial and returns AppliedDaemonless. RED without\n    // the daemonless path: apply_staged would block on connect_retry against a\n    // dead socket (300×10ms) and then fail — never landing the bytes. Proves the\n    // swap no longer depends on a live broker to hand off to.\n    #[test]\n    fn apply_staged_daemonless_swaps_binary_with_no_broker() {\n        let dir = tempfile::tempdir().unwrap();\n        let exe = dir.path().join(\"spt-binary\");\n        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n\n        // No served_broker — deliberately. A daemonless apply must not need one.\n        let out = apply_staged_daemonless(&cache, &keys, &exe).expect(\"apply ok\");\n        assert!(\n            matches!(out, ApplyStagedOutcome::AppliedDaemonless { version: 7, .. }),\n            \"got {out:?}\"\n        );\n        assert_eq!(\n            std::fs::read(&exe).unwrap(),\n            b\"NEW BINARY BYTES\",\n            \"artifact landed without any broker handoff\"\n        );\n        assert_eq!(\n            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n            b\"OLD BINARY\",\n            \"the outgoing binary stepped aside\"\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,null,117,118,null,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,null,798,null,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,null,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,null,956]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\applyhost.rs"}}},"isError":false,"timestamp":1784195766624}}
{"type":"message","id":"6885660f","parentId":"1fa51143","timestamp":"2026-07-16T09:56:06.624Z","message":{"role":"toolResult","toolCallId":"call_tsJAVCaH0YdbDg3m9nUi4ZAJ|fc_0cc7a0c9a797f232016a58aab65e74819aa0e3b82f778d2803","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":1784195766624}}
{"type":"message","id":"93085fdd","parentId":"6885660f","timestamp":"2026-07-16T09:56:20.644Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying relevant documentation update**"},{"type":"toolCall","id":"call_gexcEeLjCwzEWYiI2JIoPOcj|fc_0cc7a0c9a797f232016a58aac490b8819aac02c66612aaa1f3","name":"read","arguments":{"i":"Reading update wedge code audit","path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","selector":null}},{"type":"toolCall","id":"call_IJNW5R5tq7OhG1Q8ONSCHqYX|fc_0cc7a0c9a797f232016a58aac490e0819a98d06a7eb53469c7","name":"read","arguments":{"i":"Reading update wedge rig verdict","path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","selector":null}},{"type":"toolCall","id":"call_043rmnv7D01ZqNuyGSrYVmsL|fc_0cc7a0c9a797f232016a58aac490ec819a9e27dd9949a9e8fd","name":"glob","arguments":{"i":"Finding wedge lifecycle reports","path":"docs/**/*WEDGE*.md; docs/**/*BRAIN*.md; docs/**/*LIFECYCLE*.md","hidden":true,"gitignore":true,"limit":200}},{"type":"toolCall","id":"call_SoJYKMzlwbyC240rvzrpoZvD|fc_0cc7a0c9a797f232016a58aac490f4819ab33c6256521aa8f5","name":"read","arguments":{"i":"Reading controller output architecture","path":"crates/spt-daemon/src/broker.rs","selector":"1-190,480-620,690-880,1450-1650,2700-2730,3080-3220,3840-3910"}},{"type":"toolCall","id":"call_YysYSqEtdnpWJYQmfX3SF5Dg|fc_0cc7a0c9a797f232016a58aac490fc819aba47ef5c3438ac2d","name":"read","arguments":{"i":"Reading attach service core","path":"crates/spt-daemon/src/attach.rs","selector":"1-205,205-455"}},{"type":"toolCall","id":"call_MRWb81sjOYfAJhITBrONk702|fc_0cc7a0c9a797f232016a58aac49124819aa1abfe35b9ddb22a","name":"read","arguments":{"i":"Reading rc pump lifecycle","path":"crates/spt/src/rc.rs","selector":"1-45,850-950,1080-1280,1280-1470"}},{"type":"toolCall","id":"call_MOC5jtQLC5KPLopU0DhRGwT7|fc_0cc7a0c9a797f232016a58aac49134819a8dcc0f41a3009894","name":"read","arguments":{"i":"Reading broker connection dispatch","path":"crates/spt-daemon/src/broker.rs","selector":"2910-3225,3760-3910"}},{"type":"toolCall","id":"call_PNqGlAvU8TAnHttHh4vn6nmD|fc_0cc7a0c9a797f232016a58aac49168819aafa84c8032762551","name":"read","arguments":{"i":"Reading brain connection methods","path":"crates/spt-daemon/src/brain.rs","selector":"320-420,600-770,1220-1480"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":54822,"output":433,"cacheRead":105984,"cacheWrite":0,"totalTokens":161239,"reasoningTokens":35,"cost":{"input":0.27411,"output":0.01299,"cacheRead":0.052992,"cacheWrite":0,"total":0.340092}},"stopReason":"toolUse","timestamp":1784195766674,"responseId":"resp_0cc7a0c9a797f232016a58aab6fb28819aa4642154b18a3c6b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cc7a0c9a797f232016a58aabad694819a82840976ef1f353d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrEX9obWgS8yFVZyR8ZLxn1-8GSp8ohwLJwXlwFBjEy9OaAAjlEPnOW8O3pRongrVx7tMGDAxmLmogQ0q-3vaincNDWtjwpch_kVyoFZ9u1uxmkNg4QK7d0C3oWme2FAca62YI2HVo88QpxFgB-L3mPArdzDZIuG5ECGj01Q8i6bwjFGQICV6n5ZbaSM35lQVMplSHNSgU3wJNeJZEWj6sbbnA36sCiBZ99ZDu-2XmovmwIx3LMfAtUG0wgm8GUWzhdHN_X_qlWtbftNwzMS6jCu2GENtgZRNI8eb0YUITQtyJCuFhnqV1CHv8n9esup2JUleNKGejDwb6EXD4r3RyppPa4N5-4vVLVio2lNtsSZ9PTiRIG3yiwOne961znDdx3zky249js2Mq-x3eVbVGcJBHZk1bybXe9tK_RiA4r_uCZNgr8h6kVI6phBytKuVO-kbP-cQGqywNLG5kTY1m-3cE6j9XNU-g0ag1b_FpZUseKkGLDITLwrrZ51-0mWwYZIfanSgFC5s4OFwAOXWEwfLq1_VHDIGul97EnzNIY-61GW3-xPuagC5r5wEdg83LbGjn29rLc2fX3a6PNIAGrUp361c1TNT2WIQEuea1X0wNoZ_MaRZHEce26CcbAQ4KmnaiFmRpAx3A2_Pl3Yr8neD5Zrdbk3oDOe6MZRnMAADa3kCrXQvAl2VHGy2W1nyTEzwC1xqbJj5ZaIOsDSvUSkFL1a3FsGM1o9vE9oFUePxcjNHiLz2zJumpfN0FKWlwViB7A6AAdFLGjI0e-HEB42FtLL6a3l9n71p0J1cl0bo_DPHgmbqs432kPh66zTUV78PLavBglCXRmWc4o4PGxXlsyJ6VsDZM9Ugk7zjRv7QK0ObUvWr6o3U78ZINAENgiO1tVVRjOlzYeBW1FvR-TLHzla6dVnmKnFl4pd2idP_qW8r6fqAbJhz9pgwTY7-Y09XVzBWnMMZFwpL_RIvRUVs7HAmOeUmlVvLyvlnakIzLIkRAuit1mPzOdRTT2LpQRYWQEUNyZgdC03CeiCkiZxJzw2XCBw6K_SpV4_Odi8KJC8UK3_LE7K7qx2v1QYN8nX4y6_cHe_VsEiAqbN7J3wihtPAP2-SVO5Z4qL_FDKK8=","summary":[{"type":"summary_text","text":"**Identifying relevant documentation update**"}]},{"id":"fc_0cc7a0c9a797f232016a58aac490b8819aac02c66612aaa1f3","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update wedge code audit\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\",\"selector\":null}","call_id":"call_gexcEeLjCwzEWYiI2JIoPOcj","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aac490e0819a98d06a7eb53469c7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update wedge rig verdict\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\",\"selector\":null}","call_id":"call_IJNW5R5tq7OhG1Q8ONSCHqYX","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aac490ec819a9e27dd9949a9e8fd","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding wedge lifecycle reports\",\"path\":\"docs/**/*WEDGE*.md; docs/**/*BRAIN*.md; docs/**/*LIFECYCLE*.md\",\"hidden\":true,\"gitignore\":true,\"limit\":200}","call_id":"call_043rmnv7D01ZqNuyGSrYVmsL","name":"glob"},{"id":"fc_0cc7a0c9a797f232016a58aac490f4819ab33c6256521aa8f5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controller output architecture\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1-190,480-620,690-880,1450-1650,2700-2730,3080-3220,3840-3910\"}","call_id":"call_SoJYKMzlwbyC240rvzrpoZvD","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aac490fc819aba47ef5c3438ac2d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach service core\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"1-205,205-455\"}","call_id":"call_YysYSqEtdnpWJYQmfX3SF5Dg","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aac49124819aa1abfe35b9ddb22a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc pump lifecycle\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1-45,850-950,1080-1280,1280-1470\"}","call_id":"call_MRWb81sjOYfAJhITBrONk702","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aac49134819a8dcc0f41a3009894","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker connection dispatch\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"2910-3225,3760-3910\"}","call_id":"call_MOC5jtQLC5KPLopU0DhRGwT7","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aac49168819aafa84c8032762551","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain connection methods\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"320-420,600-770,1220-1480\"}","call_id":"call_PNqGlAvU8TAnHttHh4vn6nmD","name":"read"}]},"duration":13969.099799999967,"ttft":4141.848300000653,"contextSnapshot":{"promptTokens":160806,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gexcEeLjCwzEWYiI2JIoPOcj|fc_0cc7a0c9a797f232016a58aac490b8819aac02c66612aaa1f3","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md"},"intent":"Reading update wedge code audit"},"id":"4d783f62","parentId":"93085fdd","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IJNW5R5tq7OhG1Q8ONSCHqYX|fc_0cc7a0c9a797f232016a58aac490e0819a98d06a7eb53469c7","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md"},"intent":"Reading update wedge rig verdict"},"id":"f87d7ad0","parentId":"4d783f62","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_043rmnv7D01ZqNuyGSrYVmsL|fc_0cc7a0c9a797f232016a58aac490ec819a9e27dd9949a9e8fd","toolName":"glob","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"docs/**/*WEDGE*.md; docs/**/*BRAIN*.md; docs/**/*LIFECYCLE*.md"},"intent":"Finding wedge lifecycle reports"},"id":"8282559f","parentId":"f87d7ad0","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_SoJYKMzlwbyC240rvzrpoZvD|fc_0cc7a0c9a797f232016a58aac490f4819ab33c6256521aa8f5","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading controller output architecture"},"id":"b643f7ef","parentId":"8282559f","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YysYSqEtdnpWJYQmfX3SF5Dg|fc_0cc7a0c9a797f232016a58aac490fc819aba47ef5c3438ac2d","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach service core"},"id":"8e861222","parentId":"b643f7ef","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MRWb81sjOYfAJhITBrONk702|fc_0cc7a0c9a797f232016a58aac49124819aa1abfe35b9ddb22a","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc pump lifecycle"},"id":"3c5a4750","parentId":"8e861222","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MOC5jtQLC5KPLopU0DhRGwT7|fc_0cc7a0c9a797f232016a58aac49134819a8dcc0f41a3009894","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker connection dispatch"},"id":"d6de78c4","parentId":"3c5a4750","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PNqGlAvU8TAnHttHh4vn6nmD|fc_0cc7a0c9a797f232016a58aac49168819aafa84c8032762551","toolName":"read","startedAt":"2026-07-16T09:56:20.644Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading brain connection methods"},"id":"4f69d133","parentId":"d6de78c4","timestamp":"2026-07-16T09:56:20.644Z"}
{"type":"message","id":"77d4d3ed","parentId":"4f69d133","timestamp":"2026-07-16T09:56:20.695Z","message":{"role":"toolResult","toolCallId":"call_MRWb81sjOYfAJhITBrONk702|fc_0cc7a0c9a797f232016a58aac49124819aa1abfe35b9ddb22a","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…\nimpl Drop for RawGuard {\n    fn drop(&mut self) {\n…\n        if let Some(prior) = self.prior_out_mode {\n            restore_out_mode(prior);\n        }\n        if self.raw {\n            let _ = crossterm::terminal::disable_raw_mode();\n        }\n    }\n}\n\n/// Resolve a live endpoint id to its broker session id via the broker's hosted\n/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no\n/// live session under that endpoint.\nfn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {\n    let reply = brain.sessions()?;\n    Ok(reply\n        .sessions\n        .into_iter()\n        .find(|s| s.endpoint == endpoint_id)\n        .map(|s| s.session_id))\n}\n\n/// A thin, reusable broker connection for polling \"is there a live session for\n/// this endpoint?\" — the W4 attach-on-session-exists gate\n/// (REQ-ENDPOINT-UNBOUND-ATTACH). One broker connect, then [`has_session`] is\n/// queried across the bounded await loop (so the await never re-dials per\n/// iteration). Keys on the broker sessions map (ADR-0025 W3a) — independent of\n/// perch status, so an UNBOUND skeleton's live session reports attachable.\n///\n/// [`has_session`]: SessionProbe::has_session\npub(crate) struct SessionProbe {\n    brain: Brain,\n}\n\nimpl SessionProbe {\n    /// Cold-connect to the broker once; the connection is reused across the\n    /// await poll. `Err` ⇒ the broker is unreachable (the caller fails loud).\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn connect() -> Result<Self, String> {\n        let brain = Brain::cold_start_pump(\n            &spt_daemon::endpoint::broker_socket_name(),\n            now_ms(),\n            Duration::from_secs(10),\n        )\n        .map_err(|e| format!(\"broker connect: {e}\"))?;\n        Ok(Self { brain })\n    }\n\n    /// `true` when a live broker session exists for `endpoint_id` (ANY perch\n    /// status — an UNBOUND session counts). A query error reads as \"not yet\".\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn has_session(&mut self, endpoint_id: &str) -> bool {\n        resolve_session(&mut self.brain, endpoint_id)\n            .ok()\n            .flatten()\n            .is_some()\n    }\n}\n\n/// The node currently driving `endpoint_id`, if any — read off the perch's\n/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side\n/// busy-refuse guidance. `None` ⇒ free (or no perch).\nfn current_driver(endpoint_id: &str) -> Option<String> {\n    let perch = spt_store::perch::resolve_perch_path(\n        endpoint_id,\n        spt_store::perch::ParentHint::Infer,\n    );\n    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)\n}\n\n/// What one stdin chunk yielded after the detach state machine: the bytes to\n/// forward to the session, and whether the detach keybind fired.\nstruct DetachParse {\n    forward: Vec<u8>,\n    detach: bool,\n}\n\n/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds\n/// the \"saw the prefix\" state across chunk boundaries (a prefix can end one read\n/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →\n/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other\n/// byte passes through. Pure so it unit-tests without a terminal. On detach the\n/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the\n/// viewport is ending).\nfn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n    let mut forward = Vec::with_capacity(input.len());\n    for &b in input {\n        if *armed {\n            *armed = false;\n            match b {\n                DETACH_KEY => return DetachParse { forward, detach: true },\n                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n                other => {\n                    forward.push(DETACH_PREFIX);\n                    forward.push(other);\n                }\n            }\n        } else if b == DETACH_PREFIX {\n            *armed = true;\n        } else {\n            forward.push(b);\n        }\n    }\n…\n}\n…\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // message, never the historical infinite blank.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n                 `spt endpoint run` to restart it]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::NoLiveSession) => {\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n            // no live session for the endpoint on the node we reached, or its gate\n            // denied us. On the cross-node leg this is the D6 stale-row case.\n            match &remote_node {\n                Some(node) => {\n                    let _ = writeln!(\n                        stdout,\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n                         have ended, or the registry row is stale]\"\n                    );\n                }\n                None => {\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n                }\n            }\n            Ok(())\n        }\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\n    }\n}\n\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n/// observable situation + next action; every OTHER error passes through\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n/// the mapping is unit-tested.\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\n    if e.contains(\"brain IPC read deadline\") {\n        format!(\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\n        )\n    } else {\n        e\n    }\n}\n\n/// An established attach viewport: the broker conn, the open+subscribed attach\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n/// viewport). [impl->REQ-RC-RECONNECT]\nstruct Established {\n    brain: Brain,\n    ops: OpMinter,\n    stream_id: u64,\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\n    /// + the honest-failure copy); `None` ⇒ local.\n    remote_node: Option<String>,\n}\n\n/// Why [`establish_attach`] failed.\nenum EstablishFail {\n    /// No attachable target (no live session anywhere / ambiguous / node\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n    /// the reconnect loop retries it inside the window instead (the node may\n    /// still be coming back).\n    NoTarget(String),\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n…\n                        })?\n                        .conn_id,\n…\n                },\n            };\n…\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n…\n}"}],"details":{"displayContent":{"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…\nimpl Drop for RawGuard {\n    fn drop(&mut self) {\n…\n        if let Some(prior) = self.prior_out_mode {\n            restore_out_mode(prior);\n        }\n        if self.raw {\n            let _ = crossterm::terminal::disable_raw_mode();\n        }\n    }\n}\n\n/// Resolve a live endpoint id to its broker session id via the broker's hosted\n/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no\n/// live session under that endpoint.\nfn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {\n    let reply = brain.sessions()?;\n    Ok(reply\n        .sessions\n        .into_iter()\n        .find(|s| s.endpoint == endpoint_id)\n        .map(|s| s.session_id))\n}\n\n/// A thin, reusable broker connection for polling \"is there a live session for\n/// this endpoint?\" — the W4 attach-on-session-exists gate\n/// (REQ-ENDPOINT-UNBOUND-ATTACH). One broker connect, then [`has_session`] is\n/// queried across the bounded await loop (so the await never re-dials per\n/// iteration). Keys on the broker sessions map (ADR-0025 W3a) — independent of\n/// perch status, so an UNBOUND skeleton's live session reports attachable.\n///\n/// [`has_session`]: SessionProbe::has_session\npub(crate) struct SessionProbe {\n    brain: Brain,\n}\n\nimpl SessionProbe {\n    /// Cold-connect to the broker once; the connection is reused across the\n    /// await poll. `Err` ⇒ the broker is unreachable (the caller fails loud).\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn connect() -> Result<Self, String> {\n        let brain = Brain::cold_start_pump(\n            &spt_daemon::endpoint::broker_socket_name(),\n            now_ms(),\n            Duration::from_secs(10),\n        )\n        .map_err(|e| format!(\"broker connect: {e}\"))?;\n        Ok(Self { brain })\n    }\n\n    /// `true` when a live broker session exists for `endpoint_id` (ANY perch\n    /// status — an UNBOUND session counts). A query error reads as \"not yet\".\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn has_session(&mut self, endpoint_id: &str) -> bool {\n        resolve_session(&mut self.brain, endpoint_id)\n            .ok()\n            .flatten()\n            .is_some()\n    }\n}\n\n/// The node currently driving `endpoint_id`, if any — read off the perch's\n/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side\n/// busy-refuse guidance. `None` ⇒ free (or no perch).\nfn current_driver(endpoint_id: &str) -> Option<String> {\n    let perch = spt_store::perch::resolve_perch_path(\n        endpoint_id,\n        spt_store::perch::ParentHint::Infer,\n    );\n    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)\n}\n\n/// What one stdin chunk yielded after the detach state machine: the bytes to\n/// forward to the session, and whether the detach keybind fired.\nstruct DetachParse {\n    forward: Vec<u8>,\n    detach: bool,\n}\n\n/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds\n/// the \"saw the prefix\" state across chunk boundaries (a prefix can end one read\n/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →\n/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other\n/// byte passes through. Pure so it unit-tests without a terminal. On detach the\n/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the\n/// viewport is ending).\nfn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n    let mut forward = Vec::with_capacity(input.len());\n    for &b in input {\n        if *armed {\n            *armed = false;\n            match b {\n                DETACH_KEY => return DetachParse { forward, detach: true },\n                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n                other => {\n                    forward.push(DETACH_PREFIX);\n                    forward.push(other);\n                }\n            }\n        } else if b == DETACH_PREFIX {\n            *armed = true;\n        } else {\n            forward.push(b);\n        }\n    }\n…\n}\n…\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // message, never the historical infinite blank.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n                 `spt endpoint run` to restart it]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::NoLiveSession) => {\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n            // no live session for the endpoint on the node we reached, or its gate\n            // denied us. On the cross-node leg this is the D6 stale-row case.\n            match &remote_node {\n                Some(node) => {\n                    let _ = writeln!(\n                        stdout,\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n                         have ended, or the registry row is stale]\"\n                    );\n                }\n                None => {\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n                }\n            }\n            Ok(())\n        }\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\n    }\n}\n\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n/// observable situation + next action; every OTHER error passes through\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n/// the mapping is unit-tested.\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\n    if e.contains(\"brain IPC read deadline\") {\n        format!(\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\n        )\n    } else {\n        e\n    }\n}\n\n/// An established attach viewport: the broker conn, the open+subscribed attach\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n/// viewport). [impl->REQ-RC-RECONNECT]\nstruct Established {\n    brain: Brain,\n    ops: OpMinter,\n    stream_id: u64,\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\n    /// + the honest-failure copy); `None` ⇒ local.\n    remote_node: Option<String>,\n}\n\n/// Why [`establish_attach`] failed.\nenum EstablishFail {\n    /// No attachable target (no live session anywhere / ambiguous / node\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n    /// the reconnect loop retries it inside the window instead (the node may\n    /// still be coming back).\n    NoTarget(String),\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n…\n                        })?\n                        .conn_id,\n…\n                },\n            };\n…\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\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,null,841,842,null,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,null,955,null,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,null,1473,1474,null,1484,1485,null,1487,1488,1489,null,1501]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784195780695}}
{"type":"message","id":"9b3e806e","parentId":"77d4d3ed","timestamp":"2026-07-16T09:56:20.696Z","message":{"role":"toolResult","toolCallId":"call_SoJYKMzlwbyC240rvzrpoZvD|fc_0cc7a0c9a797f232016a58aac490f4819ab33c6256521aa8f5","toolName":"read","content":[{"type":"text","text":"//! The broker kernel (B1 + B2) — the stable half of the broker/brain split.\n//!\n//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n//! would lose continuity on if the logic restarted: the PTY master+writer and\n//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n//! client / listening sockets. It runs **no logic** — no routing, registry,\n//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n//! forward its output to the brain, inject the brain's input, report exit.\n//!\n//! ## Gapless handoff (B2 — the load-bearing invariant)\n//!\n//! A brain may be killed and restarted at any moment (a routine self-update, a\n//! crash). The broker — and every hosted child — must survive that untouched,\n//! and the new brain must re-attach to the output stream **gaplessly and\n//! exactly-once** (Spike #1 made real). Two mechanisms:\n//!\n//! - **Sequenced, buffered output.** Each session's output is appended to an\n//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n//!   in a bounded ring. The current subscriber (a brain connection) receives\n//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n//!   reach the brain in strict `seq` order with no interleave.\n//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n//!   live. The brain dedups by `seq`. Output produced *while no brain was\n//!   attached* sits in the ring and is replayed on the next subscribe — so a\n//!   brain restart loses nothing.\n//!\n//! Sessions are therefore **broker-owned and outlive any one connection**: a\n//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n//! output log persist. A session leaves the table only when its child exits\n//! (reaped by the exit waiter) or is explicitly killed.\n// [impl->REQ-DAEMON-2]\n\nuse std::collections::{HashMap, HashSet, VecDeque};\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\nuse std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\nuse std::sync::{Arc, Mutex};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant};\n\nuse interprocess::local_socket::{prelude::*, Stream};\nuse spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n\nuse spt_net::net::attach::AttachIntent;\nuse spt_net::net::endpoint::EndpointAddr;\nuse spt_store::perch::{resolve_perch_path, ParentHint};\n\nuse crate::codec::read_frame;\nuse crate::conn::{BrokerConn, ConnWrite};\nuse crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\nuse crate::frame::{Envelope, Role};\nuse crate::msg::{\n    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\n    evicted_envelope, net_dialed_envelope, output_envelope,\n    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n    KIND_NET_SENT,\n    KIND_ADAPTER_APPLY, KIND_APPLIED,\n    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n};\nuse crate::nethost::{NetHost, NET_EFFECT_SESSION};\nuse crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\nuse crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};\n\n/// The shared, serialized send side of one brain connection. Output frames,\n/// replay frames, command acks, and exit/error events all write through this.\n/// `pub(crate)`: the net host's stream logs (D4b) live-send through the same\n/// handle under the same gate discipline. Round-4\n/// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): no longer a raw\n/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n/// matching stays `Arc::ptr_eq`.\npub(crate) type SharedSend = Arc<BrokerConn>;\n\n/// Default bounded output-log depth (chunks). Large enough that a brain restart\n/// window never evicts unreplayed output in practice; the bound is what keeps a\n/// disconnected session from buffering without limit.\nconst DEFAULT_LOG_CHUNKS: usize = 4096;\n\n/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n/// owns the replay batch), so a viewer attaching to a busy session is not\n/// spuriously evicted.\nconst VIEWER_CHANNEL_DEPTH: usize = 256;\n\n/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\nconst MAX_VIEWERS: usize = 32;\n\n/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n/// the live stream) or a `Disconnected` one (its writer thread ended after a\n/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n/// is the pure decision the fan-out applies under the log lock.\n// [impl->REQ-HAZARD-VIEWER-ISOLATION]\nfn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n    r.is_err()\n}\n\n/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n/// only its own queue — never the drain. When the queue overflows the sink is\n/// dropped (the `tx` closes → the writer ends) and removed from the map.\nstruct ViewerSink {\n    /// The bounded live-frame queue to this viewer's writer thread.\n    tx: SyncSender<Envelope>,\n    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    /// (a dropped brain connection removes its viewer rows); never written here.\n    send: SharedSend,\n    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n    /// on its `rx.recv()` Err, reads this and — when true — emits a\n    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n    /// the serving brain skips to live instead of tearing the attach down.\n    evicted: Arc<AtomicBool>,\n    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n    /// a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// The controller live-frame channel depth (frames). Deeper than a viewer's\n/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is\n/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a\n/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n/// never trips the deadline.\nconst CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n\n/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but\n/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is\n/// untouched.\nconst CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n\n/// How long a controller's writer thread may sit BLOCKED inside a single socket\n/// write — the brain subscriber suspended / black-holed — before a take/reattach\n/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n/// protects the fan-out against ONE per-client conn falling behind and is\n/// output-driven (the drain observes a Full channel); THIS bound is measured\n/// full-with-**zero writer progress** and covers the case the output-driven path\n/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nconst BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n\n/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n/// the slot) is provable in ~1 s instead of 15 (mirrors the\n/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n/// ONLY — never relaxes an assertion.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn brain_write_deadline() -> Duration {\n    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n        Ok(ms) => ms\n            .parse::<u64>()\n            .map(Duration::from_millis)\n            .unwrap_or(BRAIN_WRITE_DEADLINE),\n        Err(_) => BRAIN_WRITE_DEADLINE,\n    }\n}\n\n…\n/// Pure strike-budget verdict: a MISS becomes a REAL fault once `strikes` reaches\n/// the `budget` (F029 C-1, REQ-TRANSLATE-COMMIT-MISS-TOLERANCE). Below the budget\n/// the miss is tolerated (binary preserved, worker continues).\nfn miss_strikes_exhausted(strikes: u32, budget: u32) -> bool {\n    strikes >= budget\n}\n\n/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING\n/// socket write so a backed-up controller can never stall the session drain\n/// thread (the v0.12.x wedge: the controller write was inline on the drain,\n/// holding `Mutex<OutputLog>`). Unlike a viewer it is AUTHORITATIVE: its writer\n/// advances `delivered_through` on each successful write, and it is never evicted\n/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].\nstruct ControllerSink {\n    /// The bounded live-frame queue to the controller's writer thread.\n    tx: SyncSender<CtrlMsg>,\n    /// The controller's IPC send half — kept for identity matching on detach and\n    /// for the `exit`-frame fan-out ([`OutputLog::all_sinks`]).\n    send: SharedSend,\n    /// The controller's identity: `Some(node)` = a remote operator, `None` =\n    /// local (the brain's spawn-reader / resume). Ownership keys on this.\n    by: Option<String>,\n    /// Time of the last SUCCESSFUL handoff (`try_send` Ok) to this controller's\n    /// writer channel (b4, REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE).\n    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n    /// floor). Seeded `now` at [`OutputLog::become_controller`].\n    last_ok: Instant,\n    /// The writer's IN-FLIGHT socket-write marker (LIFECYCLE-TRUTH W2,\n    /// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): the `controller_writer` sets it to\n    /// `Some(Instant::now())` immediately BEFORE it enters a blocking `write_frame`\n    /// and back to `None` the instant the write returns. A reader\n    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n    /// `last_ok` stale. `Arc<Mutex<…>>` so the reader observes the writer's live\n    /// state; the mutex is taken only around the brief set/clear and the read, never\n    /// held across the socket write itself.\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n    /// The writer thread handle (held for ownership; exits when `tx` drops — the\n    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or\n    /// when a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// A bounded, sequenced output log for one session — the gapless-resume buffer\n/// AND the W2.5 controller/viewer fan-out hub (REQ-RCVIEW-1).\n///\n/// The session's single drain thread is the only producer ([`OutputLog::append`]).\n/// Output fans out to ONE interactive `controller` (the authoritative,\n/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each\n/// an isolated bounded queue + writer thread, evicted on overflow). Every\n/// controller send and viewer `try_send` happens while the enclosing\n/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\nstruct OutputLog {\n    session_id: u64,\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    /// The single interactive controller's isolated sink (W1: a dedicated writer\n    /// thread + bounded channel + authoritative cursor). `None` ⇒ undriven. The\n    /// controller's identity (`Some(node)` remote / `None` local) lives in\n    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe\n    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a\n    /// DIFFERENT remote's `Take` displaces.\n    controller: Option<ControllerSink>,\n    /// Monotonic controller generation, shared with the live `controller_writer`\n    /// thread (`Arc<AtomicU64>`). Bumped on every [`become_controller`]; the\n    /// drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it\n    /// observed and only detaches if it still matches — so a slow-controller\n    /// timeout can never unseat a NEWER controller that attached meanwhile. The\n    /// writer snapshots its own epoch and returns the moment this value moves past\n    /// it, so a SUPERSEDED writer flushes no further frames (P1c, single live\n    /// writer per connection, [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n    controller_epoch: Arc<AtomicU64>,\n    /// The read-only viewers, keyed by a broker-minted viewer id.\n    viewers: HashMap<u64, ViewerSink>,\n    /// Monotonic viewer-id source.\n    next_viewer_id: u64,\n    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).\n    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this\n    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.\n    endpoint: String,\n    /// Last-known PTY size — seeded at spawn, updated on a controller resize.\n    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox\n    /// feed); the broker holds it since the PTY exposes no size getter.\n    size: (u16, u16),\n    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a\n    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**\n    /// to the controller, +1). It is **log** state, not subscriber state, so it\n    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and\n    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead\n    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**\n    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).\n    ///\n    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**\n    /// (never decreases) — see [`advanced_cursor`]. Both invariants are\n    /// load-bearing for at-least-once output: advancing past a *failed* write to a\n    /// dying-but-not-yet-detached controller would make resume skip that chunk\n    /// forever (at-least-once → at-most-once); decreasing on a deliberate\n    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live\n    /// reader.\n    ///\n    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,\n    /// which advances it (via [`advanced_cursor`], CAS-monotonic) as it writes —\n    /// the cursor moved off the drain thread with the controller delivery itself.\n    delivered_through: Arc<AtomicU64>,\n    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\n    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\n    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\n    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\n    /// attach's initial batch is a single synthesized [`ScreenGrid::render_repaint`]\n    /// instead of the raw ring, so replaying an alt-screen TUI into a fresh terminal\n    /// no longer corrupts scrollback. Single-writer (the one drain thread), mirroring\n    /// the ring's single producer.\n    grid: ScreenGrid,\n}\n\n/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring\n/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a\n/// successful write, and **monotonically** (never below `current`). A failed\n/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an\n/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)\n/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`\n/// swallow used to drop (doyle D4-1 amendments 1–3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {\n    if write_ok {\n        current.max(seq + 1)\n    } else {\n        current\n    }\n}\n\n/// Pure divergence decision for the control/viewer stamp convergence\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\n…\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\n        OutputLog {\n…\n            size,\n            delivered_through: Arc::new(AtomicU64::new(0)),\n            grid: ScreenGrid::new(size.0, size.1),\n        }\n    }\n\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\n    ///\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\n        if from_seq != 0 || self.next_seq == 0 {\n            return None;\n        }\n        Some((self.next_seq - 1, self.grid.render_repaint()))\n    }\n\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\n    /// log lock; `None` otherwise.\n    ///\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n    /// single drain thread.\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\n    #[must_use]\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n    fn clamp_or_reset(&mut self) -> bool {\n        let within_cap = self.ring.len() <= self.cap_chunks;\n        let last_below_next = match self.ring.back() {\n            Some(&(last, _)) => last < self.next_seq,\n            None => true,\n        };\n        let ordered = match (self.ring.front(), self.ring.back()) {\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\n            _ => true,\n        };\n        if within_cap && last_below_next && ordered {\n            return false;\n        }\n        let had = self.ring.len();\n        self.ring.clear();\n        eprintln!(\n            \"broker: session {} output ring was torn by a poison-recover \\\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n            self.session_id, self.next_seq\n        );\n        true\n    }\n\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\n    fn high_water(&self) -> u64 {\n        self.next_seq\n    }\n\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\n    /// recent, well within cap).\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\n        let mut out = Vec::new();\n        for (seq, chunk) in self.ring.iter() {\n            if *seq >= from_seq {\n                out.extend_from_slice(chunk);\n            }\n        }\n        out\n    }\n\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        while self.ring.len() > self.cap_chunks {\n            self.ring.pop_front();\n        }\n        let frame = output_envelope(self.session_id, seq, bytes);\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\n        if !self.viewers.is_empty() {\n            let mut evicted: Vec<u64> = Vec::new();\n            for (vid, sink) in self.viewers.iter() {\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n                    evicted.push(*vid);\n                }\n            }\n            if !evicted.is_empty() {\n                for vid in evicted {\n                    // Mark eviction BEFORE dropping the sink: the writer's\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                    if let Some(sink) = self.viewers.remove(&vid) {\n                        sink.evicted.store(true, Ordering::Release);\n                    }\n                }\n                self.stamp_viewer_count();\n            }\n        }\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n        // slow controller falls behind the ring) unless it has been Full for the\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n        let now = Instant::now();\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n                Ok(()) => {\n                    c.last_ok = now;\n                    None\n                }\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n                        Some(epoch) // stalled past the deadline → bounded detach\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n…\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n…\n    eprintln!(\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n        crate::conn::log_stamp()\n    );\n    record_stall_evict();\n}\n\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\n/// chunk and advances the authoritative `delivered_through` on a successful\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\n/// does NOT move the resume cursor.\nenum CtrlMsg {\n    Output(u64, Envelope),\n    Control(Envelope),\n}\n\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\n/// overlap during a controller displace.\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\n    let mut cur = dt.load(Ordering::Acquire);\n    loop {\n        let next = advanced_cursor(cur, seq, true);\n        if next == cur {\n            return;\n        }\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\n            Ok(_) => return,\n            Err(actual) => cur = actual,\n        }\n    }\n}\n\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\n/// live frame is contiguous) this is identical to [`advance_delivered`].\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\n}\n\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        if let Some(seq) = seq {\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n            // a seq (dropped while Full). The frame still went to the socket (the live\n            // operator sees it), but the durable resume cursor must NOT jump past the\n            // gap — see [`contiguous_advance`].\n            contiguous_advance(&delivered_through, seq);\n        }\n    }\n    // Bounded writer-exit record (F-039 leg d): tx dropped — a displace/detach\n    // ended the live loop normally. [impl->REQ-CONN-POISON-ATTRIBUTION]\n    send.lifecycle_event(\n        \"writer-exit\",\n        &format!(\"role=controller session={sid} reason=channel-closed\"),\n    );\n}\n\n/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present\n/// ONLY when the spawn carried a `translation_binary` path; absent → the session\n/// has no idle PTY delivery and inbound SPOOLS (ADR-0022 amendment — raw inject\n/// was removed as a delivery path; it did not submit on a modern TUI).\n///\n/// Holds the shared inject floor (the controller-input buffer, also touched by\n/// `dispatch_input`), the queue that feeds inbound `<EVENT>`s to the inject worker\n/// (events serialize — never two uncommitted sequences interleaved), the shared\n/// child handle (so `dispatch_input` can fire content-free `{type:input}` pings\n/// while the worker drives an event), and the FAULT flag the worker raises when a\n/// sequence misses its [`INJECT_COMMIT_DEADLINE`] or the binary dies — after which\n/// the session's idle inbound SPOOLS (never raw-injected) and pinging stops.\nstruct Translation {\n    /// Controller-input buffer + inject-floor flag, shared with `dispatch_input`.\n    floor: Arc<Mutex<InjectFloor>>,\n    /// Queue one inbound `<EVENT>` envelope to the inject worker. Events QUEUE here\n    /// and are driven one-at-a-time so two sequences never interleave.\n    event_tx: Sender<String>,\n    /// The live child — shared so `dispatch_input` fires content-free pings while\n    /// the worker (which also holds an `Arc`) drives events / faults the binary.\n    child: Arc<TranslationChild>,\n    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the\n    /// session's idle inbound then SPOOLS (never raw-injected) and `dispatch_input`\n    /// stops pinging.\n    faulted: Arc<AtomicBool>,\n}\n\n/// A child the broker hosts under a PTY — the §B-owned resources for one\n/// session, plus its gapless-resume output log.\nstruct HostedSession {\n    /// The PTY + child (PTY master/writer + spawned child process).\n    session: Arc<PtySession>,\n…\n}\n…\n    pub fn bind_in_with_net_cap(\n…\n    ) -> io::Result<Arc<Self>> {\n…\n        Ok(Arc::new(Broker {\n…\n            conn_handlers: AtomicUsize::new(0),\n        }))\n    }\n\n    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n    /// reports `honored: false` (no supervisor to respawn the child).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn set_brain_restart(&self, signal: Arc<crate::brainproc::BrainRestart>) -> bool {\n        self.brain_restart.set(signal).is_ok()\n    }\n\n    /// Attach a network host to a broker that was bound net-less (REQ-DAEMON-9):\n    /// the boot-race self-heal path calls this once the background retry brings\n    /// the net endpoint up, after which the dispatcher + peer pump are spawned.\n    /// Idempotent by `OnceLock` — a second attach is a no-op returning `false`\n    /// (the first host wins); the hot read path (`net()`) never blocks on it.\n    // [impl->REQ-DAEMON-9]\n    pub fn attach_net(&self, host: NetHost) -> bool {\n        self.net.set(host).is_ok()\n    }\n\n    /// The broker-owned exactly-once anchor (for introspection / tests).\n    pub fn journal(&self) -> &Arc<EffectJournal> {\n        &self.journal\n    }\n\n    /// The broker-owned network host, if one is attached (at bind or via the\n    /// REQ-DAEMON-9 self-heal `attach_net`). Lock-free.\n…\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        loop {\n…\n            match env.kind.as_str() {\n…\n                KIND_PAIR_MEET => {\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…\n    fn dispatch_net_status(&self, send: &SharedSend) {\n…\n        send_frame(send, &frame);\n    }\n\n    /// Report the broker's OWN compiled image version (F-025). `env!` bakes the\n    /// version at build time, so a broker that SURVIVED a `spt update apply`\n    /// (brain-only restart, ADR-0018 D3-3) honestly reports the PRE-apply version\n    /// — the authoritative running-image truth the `daemon status` version surface\n    /// compares against the freshly-installed CLI. Sourced from the running\n    /// process, never inferred from disk (the disk is the half already ahead).\n    // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n    fn dispatch_broker_image(&self, send: &SharedSend) {\n        let frame = Envelope::new(\n            KIND_BROKER_IMAGE_REPLY,\n            serde_json::to_value(BrokerImageReply {\n                // spt-daemon's CARGO_PKG_VERSION == the workspace version this\n                // broker binary was compiled at.\n                version: env!(\"CARGO_PKG_VERSION\").to_string(),\n            })\n            .expect(\"BrokerImageReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Answer the brain-subscriber stall-evict tally query (LIFECYCLE-TRUTH W2\n    /// observability rider) from the broker-global counters — so `daemon status` can\n    /// surface a brain that never returned as a diagnosable fact, not a silent absence.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn dispatch_stall_evicts(&self, send: &SharedSend) {\n        let (count, last_ms) = stall_evict_stats();\n        let frame = Envelope::new(\n            KIND_STALL_EVICTS_REPLY,\n            serde_json::to_value(StallEvictsReply { count, last_ms })\n                .expect(\"StallEvictsReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Handle a planned brain-restart request (ADR-0018 D3-3, Q3): raise the\n    /// supervisor's restart signal so it cycles the brain child onto the swapped\n    /// binary with `start-reason=update`, then ack. `honored` reflects whether a\n    /// supervisor is wired (a bare test broker has none → accepted-but-unhonored).\n    /// The broker — which observes every respawn — stamps the `update` reason,\n    /// keeping planned-vs-crash authoritative on this side (V2).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-UPD-3]\n    fn dispatch_brain_restart(&self, send: &SharedSend) {\n        let honored = match self.brain_restart.get() {\n            Some(signal) => {\n                signal.request();\n                true\n            }\n            None => false,\n        };\n        let frame = Envelope::new(\n            KIND_BRAIN_RESTARTED,\n            serde_json::to_value(BrainRestarted { honored })\n                .expect(\"BrainRestarted serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a).\n    ///\n    /// With an `op_id` the dial is routed through the broker-owned\n    /// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,\n    /// exactly like an idempotent PTY write: a replay of the same id after a\n    /// brain crash is deduped (no second connection) and answered with the\n    /// connection the first apply opened. The `conn_id` is recorded *inside*\n    /// the journaled closure, so a concurrent deduped replay always finds it.\n    /// A deduped id whose connection this process no longer holds (a broker\n    /// restart — the journal survives, connections don't) is a typed error"}],"details":{"displayContent":{"text":"//! The broker kernel (B1 + B2) — the stable half of the broker/brain split.\n//!\n//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n//! would lose continuity on if the logic restarted: the PTY master+writer and\n//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n//! client / listening sockets. It runs **no logic** — no routing, registry,\n//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n//! forward its output to the brain, inject the brain's input, report exit.\n//!\n//! ## Gapless handoff (B2 — the load-bearing invariant)\n//!\n//! A brain may be killed and restarted at any moment (a routine self-update, a\n//! crash). The broker — and every hosted child — must survive that untouched,\n//! and the new brain must re-attach to the output stream **gaplessly and\n//! exactly-once** (Spike #1 made real). Two mechanisms:\n//!\n//! - **Sequenced, buffered output.** Each session's output is appended to an\n//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n//!   in a bounded ring. The current subscriber (a brain connection) receives\n//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n//!   reach the brain in strict `seq` order with no interleave.\n//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n//!   live. The brain dedups by `seq`. Output produced *while no brain was\n//!   attached* sits in the ring and is replayed on the next subscribe — so a\n//!   brain restart loses nothing.\n//!\n//! Sessions are therefore **broker-owned and outlive any one connection**: a\n//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n//! output log persist. A session leaves the table only when its child exits\n//! (reaped by the exit waiter) or is explicitly killed.\n// [impl->REQ-DAEMON-2]\n\nuse std::collections::{HashMap, HashSet, VecDeque};\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\nuse std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\nuse std::sync::{Arc, Mutex};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant};\n\nuse interprocess::local_socket::{prelude::*, Stream};\nuse spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n\nuse spt_net::net::attach::AttachIntent;\nuse spt_net::net::endpoint::EndpointAddr;\nuse spt_store::perch::{resolve_perch_path, ParentHint};\n\nuse crate::codec::read_frame;\nuse crate::conn::{BrokerConn, ConnWrite};\nuse crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\nuse crate::frame::{Envelope, Role};\nuse crate::msg::{\n    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\n    evicted_envelope, net_dialed_envelope, output_envelope,\n    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n    KIND_NET_SENT,\n    KIND_ADAPTER_APPLY, KIND_APPLIED,\n    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n};\nuse crate::nethost::{NetHost, NET_EFFECT_SESSION};\nuse crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\nuse crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};\n\n/// The shared, serialized send side of one brain connection. Output frames,\n/// replay frames, command acks, and exit/error events all write through this.\n/// `pub(crate)`: the net host's stream logs (D4b) live-send through the same\n/// handle under the same gate discipline. Round-4\n/// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): no longer a raw\n/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n/// matching stays `Arc::ptr_eq`.\npub(crate) type SharedSend = Arc<BrokerConn>;\n\n/// Default bounded output-log depth (chunks). Large enough that a brain restart\n/// window never evicts unreplayed output in practice; the bound is what keeps a\n/// disconnected session from buffering without limit.\nconst DEFAULT_LOG_CHUNKS: usize = 4096;\n\n/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n/// owns the replay batch), so a viewer attaching to a busy session is not\n/// spuriously evicted.\nconst VIEWER_CHANNEL_DEPTH: usize = 256;\n\n/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\nconst MAX_VIEWERS: usize = 32;\n\n/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n/// the live stream) or a `Disconnected` one (its writer thread ended after a\n/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n/// is the pure decision the fan-out applies under the log lock.\n// [impl->REQ-HAZARD-VIEWER-ISOLATION]\nfn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n    r.is_err()\n}\n\n/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n/// only its own queue — never the drain. When the queue overflows the sink is\n/// dropped (the `tx` closes → the writer ends) and removed from the map.\nstruct ViewerSink {\n    /// The bounded live-frame queue to this viewer's writer thread.\n    tx: SyncSender<Envelope>,\n    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    /// (a dropped brain connection removes its viewer rows); never written here.\n    send: SharedSend,\n    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n    /// on its `rx.recv()` Err, reads this and — when true — emits a\n    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n    /// the serving brain skips to live instead of tearing the attach down.\n    evicted: Arc<AtomicBool>,\n    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n    /// a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// The controller live-frame channel depth (frames). Deeper than a viewer's\n/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is\n/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a\n/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n/// never trips the deadline.\nconst CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n\n/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but\n/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is\n/// untouched.\nconst CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n\n/// How long a controller's writer thread may sit BLOCKED inside a single socket\n/// write — the brain subscriber suspended / black-holed — before a take/reattach\n/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n/// protects the fan-out against ONE per-client conn falling behind and is\n/// output-driven (the drain observes a Full channel); THIS bound is measured\n/// full-with-**zero writer progress** and covers the case the output-driven path\n/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nconst BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n\n/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n/// the slot) is provable in ~1 s instead of 15 (mirrors the\n/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n/// ONLY — never relaxes an assertion.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn brain_write_deadline() -> Duration {\n    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n        Ok(ms) => ms\n            .parse::<u64>()\n            .map(Duration::from_millis)\n            .unwrap_or(BRAIN_WRITE_DEADLINE),\n        Err(_) => BRAIN_WRITE_DEADLINE,\n    }\n}\n\n…\n/// Pure strike-budget verdict: a MISS becomes a REAL fault once `strikes` reaches\n/// the `budget` (F029 C-1, REQ-TRANSLATE-COMMIT-MISS-TOLERANCE). Below the budget\n/// the miss is tolerated (binary preserved, worker continues).\nfn miss_strikes_exhausted(strikes: u32, budget: u32) -> bool {\n    strikes >= budget\n}\n\n/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING\n/// socket write so a backed-up controller can never stall the session drain\n/// thread (the v0.12.x wedge: the controller write was inline on the drain,\n/// holding `Mutex<OutputLog>`). Unlike a viewer it is AUTHORITATIVE: its writer\n/// advances `delivered_through` on each successful write, and it is never evicted\n/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].\nstruct ControllerSink {\n    /// The bounded live-frame queue to the controller's writer thread.\n    tx: SyncSender<CtrlMsg>,\n    /// The controller's IPC send half — kept for identity matching on detach and\n    /// for the `exit`-frame fan-out ([`OutputLog::all_sinks`]).\n    send: SharedSend,\n    /// The controller's identity: `Some(node)` = a remote operator, `None` =\n    /// local (the brain's spawn-reader / resume). Ownership keys on this.\n    by: Option<String>,\n    /// Time of the last SUCCESSFUL handoff (`try_send` Ok) to this controller's\n    /// writer channel (b4, REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE).\n    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n    /// floor). Seeded `now` at [`OutputLog::become_controller`].\n    last_ok: Instant,\n    /// The writer's IN-FLIGHT socket-write marker (LIFECYCLE-TRUTH W2,\n    /// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): the `controller_writer` sets it to\n    /// `Some(Instant::now())` immediately BEFORE it enters a blocking `write_frame`\n    /// and back to `None` the instant the write returns. A reader\n    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n    /// `last_ok` stale. `Arc<Mutex<…>>` so the reader observes the writer's live\n    /// state; the mutex is taken only around the brief set/clear and the read, never\n    /// held across the socket write itself.\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n    /// The writer thread handle (held for ownership; exits when `tx` drops — the\n    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or\n    /// when a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// A bounded, sequenced output log for one session — the gapless-resume buffer\n/// AND the W2.5 controller/viewer fan-out hub (REQ-RCVIEW-1).\n///\n/// The session's single drain thread is the only producer ([`OutputLog::append`]).\n/// Output fans out to ONE interactive `controller` (the authoritative,\n/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each\n/// an isolated bounded queue + writer thread, evicted on overflow). Every\n/// controller send and viewer `try_send` happens while the enclosing\n/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\nstruct OutputLog {\n    session_id: u64,\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    /// The single interactive controller's isolated sink (W1: a dedicated writer\n    /// thread + bounded channel + authoritative cursor). `None` ⇒ undriven. The\n    /// controller's identity (`Some(node)` remote / `None` local) lives in\n    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe\n    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a\n    /// DIFFERENT remote's `Take` displaces.\n    controller: Option<ControllerSink>,\n    /// Monotonic controller generation, shared with the live `controller_writer`\n    /// thread (`Arc<AtomicU64>`). Bumped on every [`become_controller`]; the\n    /// drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it\n    /// observed and only detaches if it still matches — so a slow-controller\n    /// timeout can never unseat a NEWER controller that attached meanwhile. The\n    /// writer snapshots its own epoch and returns the moment this value moves past\n    /// it, so a SUPERSEDED writer flushes no further frames (P1c, single live\n    /// writer per connection, [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n    controller_epoch: Arc<AtomicU64>,\n    /// The read-only viewers, keyed by a broker-minted viewer id.\n    viewers: HashMap<u64, ViewerSink>,\n    /// Monotonic viewer-id source.\n    next_viewer_id: u64,\n    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).\n    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this\n    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.\n    endpoint: String,\n    /// Last-known PTY size — seeded at spawn, updated on a controller resize.\n    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox\n    /// feed); the broker holds it since the PTY exposes no size getter.\n    size: (u16, u16),\n    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a\n    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**\n    /// to the controller, +1). It is **log** state, not subscriber state, so it\n    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and\n    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead\n    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**\n    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).\n    ///\n    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**\n    /// (never decreases) — see [`advanced_cursor`]. Both invariants are\n    /// load-bearing for at-least-once output: advancing past a *failed* write to a\n    /// dying-but-not-yet-detached controller would make resume skip that chunk\n    /// forever (at-least-once → at-most-once); decreasing on a deliberate\n    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live\n    /// reader.\n    ///\n    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,\n    /// which advances it (via [`advanced_cursor`], CAS-monotonic) as it writes —\n    /// the cursor moved off the drain thread with the controller delivery itself.\n    delivered_through: Arc<AtomicU64>,\n    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\n    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\n    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\n    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\n    /// attach's initial batch is a single synthesized [`ScreenGrid::render_repaint`]\n    /// instead of the raw ring, so replaying an alt-screen TUI into a fresh terminal\n    /// no longer corrupts scrollback. Single-writer (the one drain thread), mirroring\n    /// the ring's single producer.\n    grid: ScreenGrid,\n}\n\n/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring\n/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a\n/// successful write, and **monotonically** (never below `current`). A failed\n/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an\n/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)\n/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`\n/// swallow used to drop (doyle D4-1 amendments 1–3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {\n    if write_ok {\n        current.max(seq + 1)\n    } else {\n        current\n    }\n}\n\n/// Pure divergence decision for the control/viewer stamp convergence\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\n…\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\n        OutputLog {\n…\n            size,\n            delivered_through: Arc::new(AtomicU64::new(0)),\n            grid: ScreenGrid::new(size.0, size.1),\n        }\n    }\n\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\n    ///\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\n        if from_seq != 0 || self.next_seq == 0 {\n            return None;\n        }\n        Some((self.next_seq - 1, self.grid.render_repaint()))\n    }\n\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\n    /// log lock; `None` otherwise.\n    ///\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n    /// single drain thread.\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\n    #[must_use]\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n    fn clamp_or_reset(&mut self) -> bool {\n        let within_cap = self.ring.len() <= self.cap_chunks;\n        let last_below_next = match self.ring.back() {\n            Some(&(last, _)) => last < self.next_seq,\n            None => true,\n        };\n        let ordered = match (self.ring.front(), self.ring.back()) {\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\n            _ => true,\n        };\n        if within_cap && last_below_next && ordered {\n            return false;\n        }\n        let had = self.ring.len();\n        self.ring.clear();\n        eprintln!(\n            \"broker: session {} output ring was torn by a poison-recover \\\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n            self.session_id, self.next_seq\n        );\n        true\n    }\n\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\n    fn high_water(&self) -> u64 {\n        self.next_seq\n    }\n\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\n    /// recent, well within cap).\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\n        let mut out = Vec::new();\n        for (seq, chunk) in self.ring.iter() {\n            if *seq >= from_seq {\n                out.extend_from_slice(chunk);\n            }\n        }\n        out\n    }\n\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        while self.ring.len() > self.cap_chunks {\n            self.ring.pop_front();\n        }\n        let frame = output_envelope(self.session_id, seq, bytes);\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\n        if !self.viewers.is_empty() {\n            let mut evicted: Vec<u64> = Vec::new();\n            for (vid, sink) in self.viewers.iter() {\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n                    evicted.push(*vid);\n                }\n            }\n            if !evicted.is_empty() {\n                for vid in evicted {\n                    // Mark eviction BEFORE dropping the sink: the writer's\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                    if let Some(sink) = self.viewers.remove(&vid) {\n                        sink.evicted.store(true, Ordering::Release);\n                    }\n                }\n                self.stamp_viewer_count();\n            }\n        }\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n        // slow controller falls behind the ring) unless it has been Full for the\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n        let now = Instant::now();\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n                Ok(()) => {\n                    c.last_ok = now;\n                    None\n                }\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n                        Some(epoch) // stalled past the deadline → bounded detach\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n…\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n…\n    eprintln!(\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n        crate::conn::log_stamp()\n    );\n    record_stall_evict();\n}\n\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\n/// chunk and advances the authoritative `delivered_through` on a successful\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\n/// does NOT move the resume cursor.\nenum CtrlMsg {\n    Output(u64, Envelope),\n    Control(Envelope),\n}\n\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\n/// overlap during a controller displace.\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\n    let mut cur = dt.load(Ordering::Acquire);\n    loop {\n        let next = advanced_cursor(cur, seq, true);\n        if next == cur {\n            return;\n        }\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\n            Ok(_) => return,\n            Err(actual) => cur = actual,\n        }\n    }\n}\n\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\n/// live frame is contiguous) this is identical to [`advance_delivered`].\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\n}\n\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        if let Some(seq) = seq {\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n            // a seq (dropped while Full). The frame still went to the socket (the live\n            // operator sees it), but the durable resume cursor must NOT jump past the\n            // gap — see [`contiguous_advance`].\n            contiguous_advance(&delivered_through, seq);\n        }\n    }\n    // Bounded writer-exit record (F-039 leg d): tx dropped — a displace/detach\n    // ended the live loop normally. [impl->REQ-CONN-POISON-ATTRIBUTION]\n    send.lifecycle_event(\n        \"writer-exit\",\n        &format!(\"role=controller session={sid} reason=channel-closed\"),\n    );\n}\n\n/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present\n/// ONLY when the spawn carried a `translation_binary` path; absent → the session\n/// has no idle PTY delivery and inbound SPOOLS (ADR-0022 amendment — raw inject\n/// was removed as a delivery path; it did not submit on a modern TUI).\n///\n/// Holds the shared inject floor (the controller-input buffer, also touched by\n/// `dispatch_input`), the queue that feeds inbound `<EVENT>`s to the inject worker\n/// (events serialize — never two uncommitted sequences interleaved), the shared\n/// child handle (so `dispatch_input` can fire content-free `{type:input}` pings\n/// while the worker drives an event), and the FAULT flag the worker raises when a\n/// sequence misses its [`INJECT_COMMIT_DEADLINE`] or the binary dies — after which\n/// the session's idle inbound SPOOLS (never raw-injected) and pinging stops.\nstruct Translation {\n    /// Controller-input buffer + inject-floor flag, shared with `dispatch_input`.\n    floor: Arc<Mutex<InjectFloor>>,\n    /// Queue one inbound `<EVENT>` envelope to the inject worker. Events QUEUE here\n    /// and are driven one-at-a-time so two sequences never interleave.\n    event_tx: Sender<String>,\n    /// The live child — shared so `dispatch_input` fires content-free pings while\n    /// the worker (which also holds an `Arc`) drives events / faults the binary.\n    child: Arc<TranslationChild>,\n    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the\n    /// session's idle inbound then SPOOLS (never raw-injected) and `dispatch_input`\n    /// stops pinging.\n    faulted: Arc<AtomicBool>,\n}\n\n/// A child the broker hosts under a PTY — the §B-owned resources for one\n/// session, plus its gapless-resume output log.\nstruct HostedSession {\n    /// The PTY + child (PTY master/writer + spawned child process).\n    session: Arc<PtySession>,\n…\n}\n…\n    pub fn bind_in_with_net_cap(\n…\n    ) -> io::Result<Arc<Self>> {\n…\n        Ok(Arc::new(Broker {\n…\n            conn_handlers: AtomicUsize::new(0),\n        }))\n    }\n\n    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n    /// reports `honored: false` (no supervisor to respawn the child).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn set_brain_restart(&self, signal: Arc<crate::brainproc::BrainRestart>) -> bool {\n        self.brain_restart.set(signal).is_ok()\n    }\n\n    /// Attach a network host to a broker that was bound net-less (REQ-DAEMON-9):\n    /// the boot-race self-heal path calls this once the background retry brings\n    /// the net endpoint up, after which the dispatcher + peer pump are spawned.\n    /// Idempotent by `OnceLock` — a second attach is a no-op returning `false`\n    /// (the first host wins); the hot read path (`net()`) never blocks on it.\n    // [impl->REQ-DAEMON-9]\n    pub fn attach_net(&self, host: NetHost) -> bool {\n        self.net.set(host).is_ok()\n    }\n\n    /// The broker-owned exactly-once anchor (for introspection / tests).\n    pub fn journal(&self) -> &Arc<EffectJournal> {\n        &self.journal\n    }\n\n    /// The broker-owned network host, if one is attached (at bind or via the\n    /// REQ-DAEMON-9 self-heal `attach_net`). Lock-free.\n…\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        loop {\n…\n            match env.kind.as_str() {\n…\n                KIND_PAIR_MEET => {\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…\n    fn dispatch_net_status(&self, send: &SharedSend) {\n…\n        send_frame(send, &frame);\n    }\n\n    /// Report the broker's OWN compiled image version (F-025). `env!` bakes the\n    /// version at build time, so a broker that SURVIVED a `spt update apply`\n    /// (brain-only restart, ADR-0018 D3-3) honestly reports the PRE-apply version\n    /// — the authoritative running-image truth the `daemon status` version surface\n    /// compares against the freshly-installed CLI. Sourced from the running\n    /// process, never inferred from disk (the disk is the half already ahead).\n    // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n    fn dispatch_broker_image(&self, send: &SharedSend) {\n        let frame = Envelope::new(\n            KIND_BROKER_IMAGE_REPLY,\n            serde_json::to_value(BrokerImageReply {\n                // spt-daemon's CARGO_PKG_VERSION == the workspace version this\n                // broker binary was compiled at.\n                version: env!(\"CARGO_PKG_VERSION\").to_string(),\n            })\n            .expect(\"BrokerImageReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Answer the brain-subscriber stall-evict tally query (LIFECYCLE-TRUTH W2\n    /// observability rider) from the broker-global counters — so `daemon status` can\n    /// surface a brain that never returned as a diagnosable fact, not a silent absence.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn dispatch_stall_evicts(&self, send: &SharedSend) {\n        let (count, last_ms) = stall_evict_stats();\n        let frame = Envelope::new(\n            KIND_STALL_EVICTS_REPLY,\n            serde_json::to_value(StallEvictsReply { count, last_ms })\n                .expect(\"StallEvictsReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Handle a planned brain-restart request (ADR-0018 D3-3, Q3): raise the\n    /// supervisor's restart signal so it cycles the brain child onto the swapped\n    /// binary with `start-reason=update`, then ack. `honored` reflects whether a\n    /// supervisor is wired (a bare test broker has none → accepted-but-unhonored).\n    /// The broker — which observes every respawn — stamps the `update` reason,\n    /// keeping planned-vs-crash authoritative on this side (V2).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-UPD-3]\n    fn dispatch_brain_restart(&self, send: &SharedSend) {\n        let honored = match self.brain_restart.get() {\n            Some(signal) => {\n                signal.request();\n                true\n            }\n            None => false,\n        };\n        let frame = Envelope::new(\n            KIND_BRAIN_RESTARTED,\n            serde_json::to_value(BrainRestarted { honored })\n                .expect(\"BrainRestarted serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a).\n    ///\n    /// With an `op_id` the dial is routed through the broker-owned\n    /// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,\n    /// exactly like an idempotent PTY write: a replay of the same id after a\n    /// brain crash is deduped (no second connection) and answered with the\n    /// connection the first apply opened. The `conn_id` is recorded *inside*\n    /// the journaled closure, so a concurrent deduped replay always finds it.\n    /// A deduped id whose connection this process no longer holds (a broker\n    /// restart — the journal survives, connections don't) is a typed error","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,null,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,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,null,679,680,null,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,null,1443,null,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,null,1689,null,2678,null,2683,null,2688,null,2700,2701,2702,2703,2704,2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2729,2730,null,2980,null,3010,null,3016,null,3076,null,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,null,3821,null,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851,3852,3853,3854,3855,3856,3857,3858,3859,3860,3861,3862,3863,3864,3865,3866,3867,3868,3869,3870,3871,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885,3886,3887,3888,3889,3890,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3903,3904,3905,3906,3907,3908,3909,3910]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784195780695}}
{"type":"message","id":"eb955f64","parentId":"9b3e806e","timestamp":"2026-07-16T09:56:20.696Z","message":{"role":"toolResult","toolCallId":"call_043rmnv7D01ZqNuyGSrYVmsL|fc_0cc7a0c9a797f232016a58aac490ec819a9e27dd9949a9e8fd","toolName":"glob","content":[{"type":"text","text":"# docs/\nUPDATE-WEDGE-2-ROUND3-CODEREAD.md\nUPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\nNEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\nBROKER-BRAIN-SPLIT-RESTORATION.md"}],"details":{"scopePath":"docs/**/*WEDGE*.md, docs/**/*BRAIN*.md, docs/**/*LIFECYCLE*.md","fileCount":4,"files":["docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/BROKER-BRAIN-SPLIT-RESTORATION.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784195780696}}
{"type":"message","id":"afe69707","parentId":"eb955f64","timestamp":"2026-07-16T09:56:20.742Z","message":{"role":"toolResult","toolCallId":"call_PNqGlAvU8TAnHttHh4vn6nmD|fc_0cc7a0c9a797f232016a58aac49168819aafa84c8032762551","toolName":"read","content":[{"type":"text","text":"    pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {\n        let mut conn = LocalSocketTransport::connect(name)?;\n        send_hello(&mut conn, Role::Brain)?;\n        Ok(Brain {\n            conn: BrainConn::Whole(conn),\n            generation: 0,\n            gen_start_ms: now_ms,\n            session_id: None,\n            next_seq: 0,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: None,\n            baseline_next_output: false,\n        })\n    }\n\n    /// Cold start in **PUMP mode**: connect, handshake, then SPLIT the stream\n    /// into the reader-thread carrier ([`BrainConn::Split`]) and arm a per-IPC-\n    /// call deadline of `io_timeout`. This is the peer pump's dedicated client —\n    /// every reply read is bounded ([`Brain::read_event_until`] via\n    /// [`Brain::call_deadline`]) so a black-holed peer can never wedge the\n    /// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a\n    /// live `Whole` after the fact — moving the `Stream` out by value to split it\n    /// is impossible behind `&mut self`), so a pump brain is split-and-bounded\n    /// for its whole life and a non-pump brain never pays the reader thread.\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    pub fn cold_start_pump(name: &str, now_ms: u64, io_timeout: Duration) -> io::Result<Self> {\n        let mut stream = LocalSocketTransport::connect(name)?;\n        send_hello(&mut stream, Role::Brain)?;\n        Ok(Brain {\n            conn: BrainConn::split_with_reader(stream),\n            generation: 0,\n            gen_start_ms: now_ms,\n            session_id: None,\n            next_seq: 0,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: Some(io_timeout),\n            baseline_next_output: false,\n        })\n    }\n\n    /// Handoff: connect, handshake, rehydrate the resume cursor + generation\n    /// from `prior`, but stamp `gen_start = now()` (NOT `prior.gen_start_ms` —\n    /// the 2.4 invariant), and re-subscribe to the session from the cursor for a\n    /// gapless resume.\n    ///\n    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n    /// integration tests) replays output through it with no later `attach`. So it\n    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n    /// ALSO subscribes from 0 over this same socket → two racing\n    /// `controller_writer`s) is fixed in the BROKER, not by dropping this\n    /// subscribe: the epoch gate ([`OutputLog::become_controller`]) guarantees a\n    /// single live writer per connection, and the seeded `session_cursors` below\n    /// routes the consumer through the dedup-below + snap-above path so any\n    /// at-least-once boundary re-send is tolerated, never a fatal reject-gap\n    /// ([`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n    ///\n    /// **Test-only (restoration D4-3).** The production resume path is\n    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n    /// never this brain→brain frame; `handoff` survives only for the integration\n    /// tests. See [`BrainState`].\n    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n        let mut conn = LocalSocketTransport::connect(name)?;\n        send_hello(&mut conn, Role::Brain)?;\n        // DEFENSE-IN-DEPTH (P1c): seed the dedup cursor at the resume point so\n        // `read_event` routes this session through the dedup-below + snap-above\n        // path (matching `resume_sessions`), not the legacy reject-gap path that\n        // turns any at-least-once boundary reorder into a fatal `output gap`.\n        let mut session_cursors = std::collections::HashMap::new();\n        session_cursors.insert(prior.session_id, prior.next_seq);\n        let mut brain = Brain {\n            conn: BrainConn::Whole(conn),\n            generation: prior.generation + 1,\n            gen_start_ms: now_ms, // fresh — prior.gen_start_ms is intentionally discarded\n            session_id: Some(prior.session_id),\n            next_seq: prior.next_seq,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors,\n            io_timeout: None,\n            baseline_next_output: false,\n        };\n        brain.subscribe(prior.session_id, prior.next_seq)?;\n        Ok(brain)\n    }\n\n    /// The wall-clock generation start (epoch ms) — always this generation's own\n    /// start time, never a rehydrated value.\n    pub fn gen_start_ms(&self) -> u64 {\n        self.gen_start_ms\n    }\n\n    /// The generation counter (0 on cold start, +1 per handoff).\n    pub fn generation(&self) -> u64 {\n        self.generation\n    }\n\n…\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n        loop {\n…\n        }\n    }\n\n    /// Read and demux one broker event, enforcing exactly-once contiguous output:\n    /// accept the next `seq` (advancing the cursor), drop an already-seen `seq`\n    /// (replay duplicate), and reject a forward gap (lost chunk).\n    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n    /// entry (a whole `loop { read_event }` shares it, so unrelated events\n    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n    /// pump mode (blocking, unbounded).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    pub fn call_deadline(&self) -> Option<Instant> {\n        self.io_timeout.map(|d| Instant::now() + d)\n    }\n\n    /// Deadline for a peer-reply read: `now + min(io_timeout, PEER_REPLY_READ_\n    /// BUDGET)` — decoupled from and shorter than [`Self::call_deadline`] so a\n    /// silent peer drops before the carrier deadline. `None` outside pump mode\n    /// (unbounded), same as `call_deadline`.\n    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n    pub fn reply_read_deadline(&self) -> Option<Instant> {\n        peer_reply_deadline(self.io_timeout, Instant::now())\n    }\n\n    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {\n        self.read_event_until(None)\n    }\n\n    /// Like [`Brain::read_event`] but each frame read is bounded by an optional\n    /// wall-clock `deadline` (pump mode passes one; everyone else passes\n    /// `None`). A frame that does not arrive before `deadline` surfaces\n    /// [`io::ErrorKind::TimedOut`] — the pump escalates that to a SUPERVISED\n    /// RESTART (a timed-out IPC read may have desynced the stream; the client\n    /// is poisoned, never retried per-peer).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n        loop {\n            let env = self.read_frame_until(deadline)?;\n            match env.kind.as_str() {\n                KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // A **resume-mode** brain (the per-session map is populated by\n                    // `resume_sessions`) routes EVERY session through the map —\n                    // including a freshly-spawned or first-seen one, which seeds\n                    // itself at its first `seq` via `entry().or_insert` (D4-2b,\n                    // doyle amendment 5: this closes the mixed-mode gap where N≥2\n                    // fresh spawns would otherwise share the single `next_seq` and\n                    // silently drop the later sessions' seq-0 output). The broker's\n                    // `Mutex<OutputLog>`-held wire order guarantees first-sight of a\n                    // new session IS seq 0, so the seed == 0 for a spawn; a resumed\n                    // session already holds its `resume_seq` seed (the `entry`\n                    // returns it, so the boundary-dedup still applies). Discipline:\n                    // **dedup-below + snap-above, NO reject-gap** — drop an\n                    // already-delivered seq (the at-least-once boundary re-send),\n                    // accept-and-snap everything `>= cursor` (the contiguous case\n                    // AND a post-eviction ring-floor jump, which the broker's\n                    // non-reordering replay makes the only legitimate forward jump).\n                    if !self.session_cursors.is_empty() {\n                        let cursor = self.session_cursors.entry(ev.session_id).or_insert(ev.seq);\n                        if ev.seq < *cursor {\n                            continue; // replay duplicate / boundary re-send — dedup\n                        }\n                        *cursor = ev.seq + 1;\n                        let bytes = decode_bytes(&ev.data_b64)\n                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                        return Ok(BrokerEvent::Output {\n                            session_id: ev.session_id,\n                            seq: ev.seq,\n                            bytes,\n                        });\n                    }\n                    // Legacy single-session path (map empty — a pure spawn/drive\n                    // seat): strict accept-next / drop-dup / reject-gap on the one\n                    // `next_seq` cursor, byte-for-byte unchanged.\n                    //\n                    // #6 (ADR-0031): a COLD attach's FIRST output frame is a\n                    // clean-screen repaint at pseudo-seq `broker.next_seq - 1` (a\n                    // forward jump from the requested `from_seq == 0`). Adopt that seq\n                    // as the baseline instead of reject-gapping it — the repaint IS\n                    // the complete current screen, nothing prior is owed. Consumed\n                    // once per (cold) attach; every subsequent LIVE frame then takes\n                    // the strict reject-gap below (a b4 drop still re-fetches — B2).\n                    if self.baseline_next_output {\n                        self.next_seq = ev.seq;\n                        self.baseline_next_output = false;\n                    }\n                    if ev.seq < self.next_seq {\n                        continue; // already processed — a replay duplicate; dedup\n                    }\n                    if ev.seq > self.next_seq {\n                        return Err(io::Error::new(\n                            io::ErrorKind::InvalidData,\n                            format!(\"output gap: got seq {} want {}\", ev.seq, self.next_seq),\n                        ));\n                    }\n                    self.next_seq = ev.seq + 1;\n                    let bytes = decode_bytes(&ev.data_b64)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Output {\n                        session_id: ev.session_id,\n                        seq: ev.seq,\n                        bytes,\n                    });\n                }\n                KIND_SPAWNED => {\n                    let ev: Spawned = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Spawned {\n                        session_id: ev.session_id,\n                        pid: ev.pid,\n                    });\n                }\n                KIND_EXIT => {\n                    let ev: ExitEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Exit {\n                        session_id: ev.session_id,\n                        code: ev.code,\n                    });\n                }\n                KIND_SUBSCRIBED => {\n                    let ev: SubscribedReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Subscribed {\n                        session_id: ev.session_id,\n                        outcome: ev.outcome,\n                    });\n                }\n                KIND_SIZE => {\n                    let ev: SizeEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Size {\n                        session_id: ev.session_id,\n                        rows: ev.rows,\n                        cols: ev.cols,\n                    });\n                }\n                KIND_DISPLACED => {\n                    let ev: DisplacedEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Displaced {\n                        session_id: ev.session_id,\n                        by: ev.by,\n                    });\n                }\n                // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n                KIND_VIEWER_EVICTED => {\n                    let ev: ViewerEvictedEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::ViewerEvicted {\n                        session_id: ev.session_id,\n                    });\n                }\n                KIND_APPLIED => {\n                    let ev: AppliedEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Applied {\n                        session_id: ev.session_id,\n                        op_id: ev.op_id,\n                        applied_now: ev.applied_now,\n                    });\n                }\n                KIND_NET_STATUS_REPLY => {\n                    let ev: NetStatusReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStatus(ev));\n                }\n                KIND_NET_DIALED => {\n                    let ev: NetDialed = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n…\n                }\n…\n            }\n        }\n    }\n…\n    pub fn pair_join(\n…\n    ) -> io::Result<PairJoinReply> {\n        self.send(\n…\n            serde_json::to_value(PairJoinReq {\n                subnet: subnet.to_string(),\n                code: code.to_string(),\n                responder_addr,\n            })\n            .expect(\"PairJoinReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::PairJoined(r) => return Ok(r),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE): MEET a seed-holder for\n    /// `subnet` — no code. The daemon resolves and holds the member's real\n    /// address; the reply is either a [`MetMember`] (proceed to the code) or a\n    /// classified failure ([`MetOutcome::Failed`]) surfaced before any code\n    /// prompt. The code routes nothing here — the meet selector is the public\n    /// `(name, TOTP-epoch)`.\n    // [impl->REQ-JOIN-TWO-PHASE] [impl->REQ-JOIN-DIAGNOSTICS]\n    pub fn pair_meet(&mut self, subnet: &str, verbose: bool) -> io::Result<MetOutcome> {\n        self.send(\n            KIND_PAIR_MEET,\n            serde_json::to_value(PairMeetReq {\n                subnet: subnet.to_string(),\n                verbose,\n            })\n            .expect(\"PairMeetReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::MetMember(m) => return Ok(MetOutcome::Met(m)),\n                BrokerEvent::PairJoined(r) => return Ok(MetOutcome::Failed(r)),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a\n    /// held meet `session_id`. The daemon dials the HELD address (no re-search)\n    /// and runs the ceremony. A wrong-code retry calls this again with the SAME\n    /// session id (ceremony only). The code never persists.\n    // [impl->REQ-JOIN-TWO-PHASE]\n    pub fn pair_submit_code(&mut self, session_id: u64, code: &str) -> io::Result<PairJoinReply> {\n        self.send(\n            KIND_PAIR_CODE_SUBMIT,\n            serde_json::to_value(PairCodeSubmit {\n                session_id,\n                code: code.to_string(),\n            })\n            .expect(\"PairCodeSubmit serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::PairJoined(r) => return Ok(r),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a). `addr` is the peer's\n    /// serialized `EndpointAddr` (as another node's `net-status` reply carries).\n    /// With `op_id` the dial is exactly-once: a replay of the same durable id\n    /// after a brain crash is deduped at the broker and answered with the\n    /// already-open connection ([`NetDialed::applied_now`] distinguishes).\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_dial(\n        &mut self,\n        addr: serde_json::Value,\n        op: Option<MintedOp>,\n    ) -> io::Result<NetDialed> {\n        self.send(\n            KIND_NET_DIAL,\n            serde_json::to_value(NetDialReq {\n                addr,\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n            })\n            .expect(\"NetDialReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetDialed(d) => return Ok(d),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Submit a NON-BLOCKING dial (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): send\n    /// the addr and return as soon as the broker acks that the concurrent dial\n    /// spawned — the OUTCOME rides the presence stream (a `PRESENCE_CONNECTED` or\n    /// `PRESENCE_DIAL_FAILED` the pump drains on its dedicated event carrier),\n    /// NEVER this reply. The pump's dials are non-journaled (`op_id: None`), so\n    /// this carries no op. Contrast [`Self::net_dial`] which BLOCKS for the\n    /// connection — the serial fan-out W2 replaces. The ack read is bounded by\n    /// the call deadline (pump mode); the broker acks immediately (before the\n    /// dial resolves), so this never waits on a peer.\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    pub fn submit_dial(&mut self, addr: serde_json::Value) -> io::Result<()> {\n        self.send(\n            KIND_NET_DIAL_SUBMIT,\n            serde_json::to_value(NetDialReq {\n                addr,\n                op_id: None,\n                minter: None,\n            })\n            .expect(\"NetDialReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetDialSubmitted => return Ok(()),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Mint (or reuse) the broker's single in-process loopback connection\n    /// (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides, because a QUIC\n    /// self-dial is refused by iroh (\"Connecting to ourself is not supported\").\n    /// The returned [`NetDialed::remote_id_hex`] is THIS node's own id, so a\n    /// stream served over it gates same-node (access short-circuit Allow). No\n    /// op id: the singleton is idempotent by reuse, and there is no network leg\n    /// a crash-replay could double. Identical from `request_attach` up — the\n    /// one-pump invariant lives at the transport leaf, not here.\n    // [impl->REQ-RC-1]\n    pub fn net_dial_loopback(&mut self) -> io::Result<NetDialed> {\n        self.send(KIND_NET_DIAL_LOOPBACK, serde_json::Value::Null)?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetDialed(d) => return Ok(d),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Open a bidi stream on a held connection (D4b). With `op_id` the open is\n    /// exactly-once (a replay is answered with the original stream). NOTE all\n    /// net op ids — dials, opens, sends — share ONE durable namespace; mint\n    /// them from a single durable counter.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_open_stream(\n        &mut self,\n        conn_id: u64,\n        op: Option<MintedOp>,\n    ) -> io::Result<NetStreamOpened> {\n        self.send(\n            KIND_NET_STREAM_OPEN,\n            serde_json::to_value(NetStreamOpenReq {\n                conn_id,\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n            })\n            .expect(\"NetStreamOpenReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Write bytes to a held stream over the broker (D4b). With `op_id` the\n    /// write is exactly-once and this blocks for the `net-sent` ack (returned);\n    /// without one it is a direct fire-and-forget write (`None` returned).\n    /// `finish` closes the send side after the write.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_stream_send(\n        &mut self,\n        stream_id: u64,\n        bytes: &[u8],\n        op: Option<MintedOp>,\n        finish: bool,\n    ) -> io::Result<Option<NetSent>> {\n        self.send(\n            KIND_NET_STREAM_SEND,\n            serde_json::to_value(NetStreamSendReq {\n                stream_id,\n                data_b64: encode_bytes(bytes),\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n                finish,\n            })\n            .expect(\"NetStreamSendReq serializes\"),\n        )?;\n        if op.is_none() {\n            return Ok(None);\n        }\n        let want_seq = op.map(|o| o.seq);\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetSent(ev) if Some(ev.op_id) == want_seq => return Ok(Some(ev)),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// List the streams the broker holds (D4b) — how a re-attaching brain\n    /// finds streams (incl. peer-initiated ones) that arrived while it was down.\n    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// 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 cold_start(name: &str, now_ms: u64) -> io::Result<Self> {\n        let mut conn = LocalSocketTransport::connect(name)?;\n        send_hello(&mut conn, Role::Brain)?;\n        Ok(Brain {\n            conn: BrainConn::Whole(conn),\n            generation: 0,\n            gen_start_ms: now_ms,\n            session_id: None,\n            next_seq: 0,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: None,\n            baseline_next_output: false,\n        })\n    }\n\n    /// Cold start in **PUMP mode**: connect, handshake, then SPLIT the stream\n    /// into the reader-thread carrier ([`BrainConn::Split`]) and arm a per-IPC-\n    /// call deadline of `io_timeout`. This is the peer pump's dedicated client —\n    /// every reply read is bounded ([`Brain::read_event_until`] via\n    /// [`Brain::call_deadline`]) so a black-holed peer can never wedge the\n    /// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a\n    /// live `Whole` after the fact — moving the `Stream` out by value to split it\n    /// is impossible behind `&mut self`), so a pump brain is split-and-bounded\n    /// for its whole life and a non-pump brain never pays the reader thread.\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    pub fn cold_start_pump(name: &str, now_ms: u64, io_timeout: Duration) -> io::Result<Self> {\n        let mut stream = LocalSocketTransport::connect(name)?;\n        send_hello(&mut stream, Role::Brain)?;\n        Ok(Brain {\n            conn: BrainConn::split_with_reader(stream),\n            generation: 0,\n            gen_start_ms: now_ms,\n            session_id: None,\n            next_seq: 0,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: Some(io_timeout),\n            baseline_next_output: false,\n        })\n    }\n\n    /// Handoff: connect, handshake, rehydrate the resume cursor + generation\n    /// from `prior`, but stamp `gen_start = now()` (NOT `prior.gen_start_ms` —\n    /// the 2.4 invariant), and re-subscribe to the session from the cursor for a\n    /// gapless resume.\n    ///\n    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n    /// integration tests) replays output through it with no later `attach`. So it\n    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n    /// ALSO subscribes from 0 over this same socket → two racing\n    /// `controller_writer`s) is fixed in the BROKER, not by dropping this\n    /// subscribe: the epoch gate ([`OutputLog::become_controller`]) guarantees a\n    /// single live writer per connection, and the seeded `session_cursors` below\n    /// routes the consumer through the dedup-below + snap-above path so any\n    /// at-least-once boundary re-send is tolerated, never a fatal reject-gap\n    /// ([`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n    ///\n    /// **Test-only (restoration D4-3).** The production resume path is\n    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n    /// never this brain→brain frame; `handoff` survives only for the integration\n    /// tests. See [`BrainState`].\n    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n        let mut conn = LocalSocketTransport::connect(name)?;\n        send_hello(&mut conn, Role::Brain)?;\n        // DEFENSE-IN-DEPTH (P1c): seed the dedup cursor at the resume point so\n        // `read_event` routes this session through the dedup-below + snap-above\n        // path (matching `resume_sessions`), not the legacy reject-gap path that\n        // turns any at-least-once boundary reorder into a fatal `output gap`.\n        let mut session_cursors = std::collections::HashMap::new();\n        session_cursors.insert(prior.session_id, prior.next_seq);\n        let mut brain = Brain {\n            conn: BrainConn::Whole(conn),\n            generation: prior.generation + 1,\n            gen_start_ms: now_ms, // fresh — prior.gen_start_ms is intentionally discarded\n            session_id: Some(prior.session_id),\n            next_seq: prior.next_seq,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors,\n            io_timeout: None,\n            baseline_next_output: false,\n        };\n        brain.subscribe(prior.session_id, prior.next_seq)?;\n        Ok(brain)\n    }\n\n    /// The wall-clock generation start (epoch ms) — always this generation's own\n    /// start time, never a rehydrated value.\n    pub fn gen_start_ms(&self) -> u64 {\n        self.gen_start_ms\n    }\n\n    /// The generation counter (0 on cold start, +1 per handoff).\n    pub fn generation(&self) -> u64 {\n        self.generation\n    }\n\n…\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n        loop {\n…\n        }\n    }\n\n    /// Read and demux one broker event, enforcing exactly-once contiguous output:\n    /// accept the next `seq` (advancing the cursor), drop an already-seen `seq`\n    /// (replay duplicate), and reject a forward gap (lost chunk).\n    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n    /// entry (a whole `loop { read_event }` shares it, so unrelated events\n    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n    /// pump mode (blocking, unbounded).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    pub fn call_deadline(&self) -> Option<Instant> {\n        self.io_timeout.map(|d| Instant::now() + d)\n    }\n\n    /// Deadline for a peer-reply read: `now + min(io_timeout, PEER_REPLY_READ_\n    /// BUDGET)` — decoupled from and shorter than [`Self::call_deadline`] so a\n    /// silent peer drops before the carrier deadline. `None` outside pump mode\n    /// (unbounded), same as `call_deadline`.\n    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n    pub fn reply_read_deadline(&self) -> Option<Instant> {\n        peer_reply_deadline(self.io_timeout, Instant::now())\n    }\n\n    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {\n        self.read_event_until(None)\n    }\n\n    /// Like [`Brain::read_event`] but each frame read is bounded by an optional\n    /// wall-clock `deadline` (pump mode passes one; everyone else passes\n    /// `None`). A frame that does not arrive before `deadline` surfaces\n    /// [`io::ErrorKind::TimedOut`] — the pump escalates that to a SUPERVISED\n    /// RESTART (a timed-out IPC read may have desynced the stream; the client\n    /// is poisoned, never retried per-peer).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n        loop {\n            let env = self.read_frame_until(deadline)?;\n            match env.kind.as_str() {\n                KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // A **resume-mode** brain (the per-session map is populated by\n                    // `resume_sessions`) routes EVERY session through the map —\n                    // including a freshly-spawned or first-seen one, which seeds\n                    // itself at its first `seq` via `entry().or_insert` (D4-2b,\n                    // doyle amendment 5: this closes the mixed-mode gap where N≥2\n                    // fresh spawns would otherwise share the single `next_seq` and\n                    // silently drop the later sessions' seq-0 output). The broker's\n                    // `Mutex<OutputLog>`-held wire order guarantees first-sight of a\n                    // new session IS seq 0, so the seed == 0 for a spawn; a resumed\n                    // session already holds its `resume_seq` seed (the `entry`\n                    // returns it, so the boundary-dedup still applies). Discipline:\n                    // **dedup-below + snap-above, NO reject-gap** — drop an\n                    // already-delivered seq (the at-least-once boundary re-send),\n                    // accept-and-snap everything `>= cursor` (the contiguous case\n                    // AND a post-eviction ring-floor jump, which the broker's\n                    // non-reordering replay makes the only legitimate forward jump).\n                    if !self.session_cursors.is_empty() {\n                        let cursor = self.session_cursors.entry(ev.session_id).or_insert(ev.seq);\n                        if ev.seq < *cursor {\n                            continue; // replay duplicate / boundary re-send — dedup\n                        }\n                        *cursor = ev.seq + 1;\n                        let bytes = decode_bytes(&ev.data_b64)\n                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                        return Ok(BrokerEvent::Output {\n                            session_id: ev.session_id,\n                            seq: ev.seq,\n                            bytes,\n                        });\n                    }\n                    // Legacy single-session path (map empty — a pure spawn/drive\n                    // seat): strict accept-next / drop-dup / reject-gap on the one\n                    // `next_seq` cursor, byte-for-byte unchanged.\n                    //\n                    // #6 (ADR-0031): a COLD attach's FIRST output frame is a\n                    // clean-screen repaint at pseudo-seq `broker.next_seq - 1` (a\n                    // forward jump from the requested `from_seq == 0`). Adopt that seq\n                    // as the baseline instead of reject-gapping it — the repaint IS\n                    // the complete current screen, nothing prior is owed. Consumed\n                    // once per (cold) attach; every subsequent LIVE frame then takes\n                    // the strict reject-gap below (a b4 drop still re-fetches — B2).\n                    if self.baseline_next_output {\n                        self.next_seq = ev.seq;\n                        self.baseline_next_output = false;\n                    }\n                    if ev.seq < self.next_seq {\n                        continue; // already processed — a replay duplicate; dedup\n                    }\n                    if ev.seq > self.next_seq {\n                        return Err(io::Error::new(\n                            io::ErrorKind::InvalidData,\n                            format!(\"output gap: got seq {} want {}\", ev.seq, self.next_seq),\n                        ));\n                    }\n                    self.next_seq = ev.seq + 1;\n                    let bytes = decode_bytes(&ev.data_b64)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Output {\n                        session_id: ev.session_id,\n                        seq: ev.seq,\n                        bytes,\n                    });\n                }\n                KIND_SPAWNED => {\n                    let ev: Spawned = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Spawned {\n                        session_id: ev.session_id,\n                        pid: ev.pid,\n                    });\n                }\n                KIND_EXIT => {\n                    let ev: ExitEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Exit {\n                        session_id: ev.session_id,\n                        code: ev.code,\n                    });\n                }\n                KIND_SUBSCRIBED => {\n                    let ev: SubscribedReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Subscribed {\n                        session_id: ev.session_id,\n                        outcome: ev.outcome,\n                    });\n                }\n                KIND_SIZE => {\n                    let ev: SizeEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Size {\n                        session_id: ev.session_id,\n                        rows: ev.rows,\n                        cols: ev.cols,\n                    });\n                }\n                KIND_DISPLACED => {\n                    let ev: DisplacedEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Displaced {\n                        session_id: ev.session_id,\n                        by: ev.by,\n                    });\n                }\n                // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n                KIND_VIEWER_EVICTED => {\n                    let ev: ViewerEvictedEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::ViewerEvicted {\n                        session_id: ev.session_id,\n                    });\n                }\n                KIND_APPLIED => {\n                    let ev: AppliedEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Applied {\n                        session_id: ev.session_id,\n                        op_id: ev.op_id,\n                        applied_now: ev.applied_now,\n                    });\n                }\n                KIND_NET_STATUS_REPLY => {\n                    let ev: NetStatusReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStatus(ev));\n                }\n                KIND_NET_DIALED => {\n                    let ev: NetDialed = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n…\n                }\n…\n            }\n        }\n    }\n…\n    pub fn pair_join(\n…\n    ) -> io::Result<PairJoinReply> {\n        self.send(\n…\n            serde_json::to_value(PairJoinReq {\n                subnet: subnet.to_string(),\n                code: code.to_string(),\n                responder_addr,\n            })\n            .expect(\"PairJoinReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::PairJoined(r) => return Ok(r),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE): MEET a seed-holder for\n    /// `subnet` — no code. The daemon resolves and holds the member's real\n    /// address; the reply is either a [`MetMember`] (proceed to the code) or a\n    /// classified failure ([`MetOutcome::Failed`]) surfaced before any code\n    /// prompt. The code routes nothing here — the meet selector is the public\n    /// `(name, TOTP-epoch)`.\n    // [impl->REQ-JOIN-TWO-PHASE] [impl->REQ-JOIN-DIAGNOSTICS]\n    pub fn pair_meet(&mut self, subnet: &str, verbose: bool) -> io::Result<MetOutcome> {\n        self.send(\n            KIND_PAIR_MEET,\n            serde_json::to_value(PairMeetReq {\n                subnet: subnet.to_string(),\n                verbose,\n            })\n            .expect(\"PairMeetReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::MetMember(m) => return Ok(MetOutcome::Met(m)),\n                BrokerEvent::PairJoined(r) => return Ok(MetOutcome::Failed(r)),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a\n    /// held meet `session_id`. The daemon dials the HELD address (no re-search)\n    /// and runs the ceremony. A wrong-code retry calls this again with the SAME\n    /// session id (ceremony only). The code never persists.\n    // [impl->REQ-JOIN-TWO-PHASE]\n    pub fn pair_submit_code(&mut self, session_id: u64, code: &str) -> io::Result<PairJoinReply> {\n        self.send(\n            KIND_PAIR_CODE_SUBMIT,\n            serde_json::to_value(PairCodeSubmit {\n                session_id,\n                code: code.to_string(),\n            })\n            .expect(\"PairCodeSubmit serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::PairJoined(r) => return Ok(r),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a). `addr` is the peer's\n    /// serialized `EndpointAddr` (as another node's `net-status` reply carries).\n    /// With `op_id` the dial is exactly-once: a replay of the same durable id\n    /// after a brain crash is deduped at the broker and answered with the\n    /// already-open connection ([`NetDialed::applied_now`] distinguishes).\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_dial(\n        &mut self,\n        addr: serde_json::Value,\n        op: Option<MintedOp>,\n    ) -> io::Result<NetDialed> {\n        self.send(\n            KIND_NET_DIAL,\n            serde_json::to_value(NetDialReq {\n                addr,\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n            })\n            .expect(\"NetDialReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetDialed(d) => return Ok(d),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Submit a NON-BLOCKING dial (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): send\n    /// the addr and return as soon as the broker acks that the concurrent dial\n    /// spawned — the OUTCOME rides the presence stream (a `PRESENCE_CONNECTED` or\n    /// `PRESENCE_DIAL_FAILED` the pump drains on its dedicated event carrier),\n    /// NEVER this reply. The pump's dials are non-journaled (`op_id: None`), so\n    /// this carries no op. Contrast [`Self::net_dial`] which BLOCKS for the\n    /// connection — the serial fan-out W2 replaces. The ack read is bounded by\n    /// the call deadline (pump mode); the broker acks immediately (before the\n    /// dial resolves), so this never waits on a peer.\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    pub fn submit_dial(&mut self, addr: serde_json::Value) -> io::Result<()> {\n        self.send(\n            KIND_NET_DIAL_SUBMIT,\n            serde_json::to_value(NetDialReq {\n                addr,\n                op_id: None,\n                minter: None,\n            })\n            .expect(\"NetDialReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetDialSubmitted => return Ok(()),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Mint (or reuse) the broker's single in-process loopback connection\n    /// (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides, because a QUIC\n    /// self-dial is refused by iroh (\"Connecting to ourself is not supported\").\n    /// The returned [`NetDialed::remote_id_hex`] is THIS node's own id, so a\n    /// stream served over it gates same-node (access short-circuit Allow). No\n    /// op id: the singleton is idempotent by reuse, and there is no network leg\n    /// a crash-replay could double. Identical from `request_attach` up — the\n    /// one-pump invariant lives at the transport leaf, not here.\n    // [impl->REQ-RC-1]\n    pub fn net_dial_loopback(&mut self) -> io::Result<NetDialed> {\n        self.send(KIND_NET_DIAL_LOOPBACK, serde_json::Value::Null)?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetDialed(d) => return Ok(d),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Open a bidi stream on a held connection (D4b). With `op_id` the open is\n    /// exactly-once (a replay is answered with the original stream). NOTE all\n    /// net op ids — dials, opens, sends — share ONE durable namespace; mint\n    /// them from a single durable counter.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_open_stream(\n        &mut self,\n        conn_id: u64,\n        op: Option<MintedOp>,\n    ) -> io::Result<NetStreamOpened> {\n        self.send(\n            KIND_NET_STREAM_OPEN,\n            serde_json::to_value(NetStreamOpenReq {\n                conn_id,\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n            })\n            .expect(\"NetStreamOpenReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Write bytes to a held stream over the broker (D4b). With `op_id` the\n    /// write is exactly-once and this blocks for the `net-sent` ack (returned);\n    /// without one it is a direct fire-and-forget write (`None` returned).\n    /// `finish` closes the send side after the write.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_stream_send(\n        &mut self,\n        stream_id: u64,\n        bytes: &[u8],\n        op: Option<MintedOp>,\n        finish: bool,\n    ) -> io::Result<Option<NetSent>> {\n        self.send(\n            KIND_NET_STREAM_SEND,\n            serde_json::to_value(NetStreamSendReq {\n                stream_id,\n                data_b64: encode_bytes(bytes),\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n                finish,\n            })\n            .expect(\"NetStreamSendReq serializes\"),\n        )?;\n        if op.is_none() {\n            return Ok(None);\n        }\n        let want_seq = op.map(|o| o.seq);\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetSent(ev) if Some(ev.op_id) == want_seq => return Ok(Some(ev)),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// List the streams the broker holds (D4b) — how a re-attaching brain\n    /// finds streams (incl. peer-initiated ones) that arrived while it was down.\n    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// 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":319,"lineNumbers":[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,null,582,583,null,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,null,772,null,872,873,874,null,1212,null,1217,1218,null,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784195780742}}
{"type":"message","id":"a12e95d2","parentId":"afe69707","timestamp":"2026-07-16T09:56:20.743Z","message":{"role":"toolResult","toolCallId":"call_MOC5jtQLC5KPLopU0DhRGwT7|fc_0cc7a0c9a797f232016a58aac49134819a8dcc0f41a3009894","toolName":"read","content":[{"type":"text","text":"\n    /// The child pid of a hosted session, if present (tests assert it is stable\n    /// across brain restarts).\n    pub fn session_pid(&self, id: u64) -> Option<u32> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .and_then(|h| h.session.process_id())\n    }\n\n    /// Broker-truth read for the brain promotion DRAINED gate\n    /// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the\n    /// brain's own `by: None` conn) still WEDGED — blocked in a socket write past the\n    /// brain-write deadline? The brain supervisor (in THIS broker process — no round-\n    /// trip) promotes a ready update candidate only once this reads `false`: an OLD\n    /// generation whose local controller conn is black-holed reads wedged until the W2\n    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\n    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\n    /// closed old conn is simply absent → `false` at once (the normal fast promote).\n    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\n    /// only `write_blocked_since` under each log lock, never any I/O.\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    pub fn any_local_controller_wedged(&self) -> bool {\n        let now = Instant::now();\n        let deadline = brain_write_deadline();\n        let sessions = match self.sessions.lock() {\n            Ok(g) => g,\n            Err(p) => p.into_inner(),\n        };\n        sessions.values().any(|h| {\n            match h.log.lock() {\n                Ok(log) => log.local_controller_wedged(now, deadline),\n                // A poisoned log lock cannot be honestly read — treat as NOT wedged\n                // (fail toward promotion, never a permanent gate that strands apply).\n                Err(_) => false,\n            }\n        })\n    }\n\n    /// The count of live QUIC connections the broker-owned net endpoint holds\n    /// (D4a, ADR-0004 §B), or 0 when net-less. A direct read of the broker's own\n    /// state — the survival-assertion seam for the D7-1 process-level E2E: a QUIC\n    /// connection the broker holds must outlive a brain-PROCESS swap (restoration\n    /// D7-1), the net sibling of [`session_count`](Self::session_count).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn net_conn_count(&self) -> usize {\n        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)\n    }\n\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    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    }\n…\n    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        match resolved {\n            Some(mut translation) => {\n…\n                // A native caller ignores the hint and decides spool-vs-drop by its\n                // prefer/force flag. Raw `payload + \\r` is NOT a fallback (it does not\n                // submit on a modern TUI — the F-019 silent pseudo-delivery); the\n                // inbound is never lost (spooled, poll-fed) and the miss is LOUD.\n                // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n                // [impl->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\n                if !idle {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: endpoint ACTIVE -> spool (deferred hint), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, true));\n                } else {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: no working translation binary (absent/faulted/worker-gone) -> SPOOLED (idle window), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                }\n                Ok(())\n            }\n            // No hosted session for this endpoint — tell the caller to spool\n            // NON-deferred (idle-eligible; a non-hosted target has no active window).\n            None => {\n                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                Ok(())\n            }\n        }\n    }\n\n    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker\n    /// rejects a resize arriving on any stream that is not the session's current\n    /// controller (a viewer's resize is refused), then updates the stored size\n    /// and pushes a `size` frame to every viewer (the letterbox feed).\n    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: ResizeReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad resize payload: {e}\"))?;\n        let (session, 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.session), Arc::clone(&h.log))\n        };\n        if !recover_log(&log).is_controller(send) {\n            return Err(\n                \"resize is controller-exclusive: only the interactive controller may resize\"\n                    .to_string(),\n            );\n        }\n        session\n            .resize(SurfaceSize::new(req.rows, req.cols))\n            .map_err(|e| format!(\"resize failed: {e}\"))?;\n        recover_log(&log).set_size_and_notify(req.rows, req.cols);\n        Ok(())\n    }\n\n    /// Report the broker-owned net endpoint's status (D4a). Answered even when\n    /// the broker has no network host (`enabled: false`) so a brain can probe\n    /// capability without treating absence as an error.\n    // [impl->REQ-DAEMON-2]\n    fn dispatch_net_status(&self, send: &SharedSend) {\n        let reply = match self.net.get() {\n            Some(host) => NetStatusReply {\n                enabled: true,\n                node_id_hex: Some(host.node_id_hex()),\n                addr: host.addr_json(),\n                conn_count: host.conn_count(),\n            },\n            None => NetStatusReply {\n                enabled: false,\n                node_id_hex: None,\n                addr: serde_json::Value::Null,\n                conn_count: 0,\n            },\n        };\n        let frame = Envelope::new(\n            KIND_NET_STATUS_REPLY,\n            serde_json::to_value(reply).expect(\"NetStatusReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Report the broker's OWN compiled image version (F-025). `env!` bakes the\n    /// version at build time, so a broker that SURVIVED a `spt update apply`\n    /// (brain-only restart, ADR-0018 D3-3) honestly reports the PRE-apply version\n    /// — the authoritative running-image truth the `daemon status` version surface\n    /// compares against the freshly-installed CLI. Sourced from the running\n    /// process, never inferred from disk (the disk is the half already ahead).\n    // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n    fn dispatch_broker_image(&self, send: &SharedSend) {\n        let frame = Envelope::new(\n            KIND_BROKER_IMAGE_REPLY,\n            serde_json::to_value(BrokerImageReply {\n                // spt-daemon's CARGO_PKG_VERSION == the workspace version this\n                // broker binary was compiled at.\n                version: env!(\"CARGO_PKG_VERSION\").to_string(),\n            })\n            .expect(\"BrokerImageReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Answer the brain-subscriber stall-evict tally query (LIFECYCLE-TRUTH W2\n    /// observability rider) from the broker-global counters — so `daemon status` can\n    /// surface a brain that never returned as a diagnosable fact, not a silent absence.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn dispatch_stall_evicts(&self, send: &SharedSend) {\n        let (count, last_ms) = stall_evict_stats();\n        let frame = Envelope::new(\n            KIND_STALL_EVICTS_REPLY,\n            serde_json::to_value(StallEvictsReply { count, last_ms })\n                .expect(\"StallEvictsReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Handle a planned brain-restart request (ADR-0018 D3-3, Q3): raise the\n    /// supervisor's restart signal so it cycles the brain child onto the swapped\n    /// binary with `start-reason=update`, then ack. `honored` reflects whether a\n    /// supervisor is wired (a bare test broker has none → accepted-but-unhonored).\n    /// The broker — which observes every respawn — stamps the `update` reason,\n    /// keeping planned-vs-crash authoritative on this side (V2).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-UPD-3]\n    fn dispatch_brain_restart(&self, send: &SharedSend) {\n        let honored = match self.brain_restart.get() {\n            Some(signal) => {\n                signal.request();\n                true\n            }\n            None => false,\n        };\n        let frame = Envelope::new(\n            KIND_BRAIN_RESTARTED,\n            serde_json::to_value(BrainRestarted { honored })\n                .expect(\"BrainRestarted serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a).\n    ///\n    /// With an `op_id` the dial is routed through the broker-owned\n    /// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,\n    /// exactly like an idempotent PTY write: a replay of the same id after a\n    /// brain crash is deduped (no second connection) and answered with the\n    /// connection the first apply opened. The `conn_id` is recorded *inside*\n    /// the journaled closure, so a concurrent deduped replay always finds it.\n    /// A deduped id whose connection this process no longer holds (a broker\n    /// restart — the journal survives, connections don't) is a typed error"}],"details":{"displayContent":{"text":"\n    /// The child pid of a hosted session, if present (tests assert it is stable\n    /// across brain restarts).\n    pub fn session_pid(&self, id: u64) -> Option<u32> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .and_then(|h| h.session.process_id())\n    }\n\n    /// Broker-truth read for the brain promotion DRAINED gate\n    /// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the\n    /// brain's own `by: None` conn) still WEDGED — blocked in a socket write past the\n    /// brain-write deadline? The brain supervisor (in THIS broker process — no round-\n    /// trip) promotes a ready update candidate only once this reads `false`: an OLD\n    /// generation whose local controller conn is black-holed reads wedged until the W2\n    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\n    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\n    /// closed old conn is simply absent → `false` at once (the normal fast promote).\n    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\n    /// only `write_blocked_since` under each log lock, never any I/O.\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    pub fn any_local_controller_wedged(&self) -> bool {\n        let now = Instant::now();\n        let deadline = brain_write_deadline();\n        let sessions = match self.sessions.lock() {\n            Ok(g) => g,\n            Err(p) => p.into_inner(),\n        };\n        sessions.values().any(|h| {\n            match h.log.lock() {\n                Ok(log) => log.local_controller_wedged(now, deadline),\n                // A poisoned log lock cannot be honestly read — treat as NOT wedged\n                // (fail toward promotion, never a permanent gate that strands apply).\n                Err(_) => false,\n            }\n        })\n    }\n\n    /// The count of live QUIC connections the broker-owned net endpoint holds\n    /// (D4a, ADR-0004 §B), or 0 when net-less. A direct read of the broker's own\n    /// state — the survival-assertion seam for the D7-1 process-level E2E: a QUIC\n    /// connection the broker holds must outlive a brain-PROCESS swap (restoration\n    /// D7-1), the net sibling of [`session_count`](Self::session_count).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn net_conn_count(&self) -> usize {\n        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)\n    }\n\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    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    }\n…\n    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        match resolved {\n            Some(mut translation) => {\n…\n                // A native caller ignores the hint and decides spool-vs-drop by its\n                // prefer/force flag. Raw `payload + \\r` is NOT a fallback (it does not\n                // submit on a modern TUI — the F-019 silent pseudo-delivery); the\n                // inbound is never lost (spooled, poll-fed) and the miss is LOUD.\n                // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n                // [impl->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\n                if !idle {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: endpoint ACTIVE -> spool (deferred hint), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, true));\n                } else {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: no working translation binary (absent/faulted/worker-gone) -> SPOOLED (idle window), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                }\n                Ok(())\n            }\n            // No hosted session for this endpoint — tell the caller to spool\n            // NON-deferred (idle-eligible; a non-hosted target has no active window).\n            None => {\n                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                Ok(())\n            }\n        }\n    }\n\n    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker\n    /// rejects a resize arriving on any stream that is not the session's current\n    /// controller (a viewer's resize is refused), then updates the stored size\n    /// and pushes a `size` frame to every viewer (the letterbox feed).\n    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: ResizeReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad resize payload: {e}\"))?;\n        let (session, 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.session), Arc::clone(&h.log))\n        };\n        if !recover_log(&log).is_controller(send) {\n            return Err(\n                \"resize is controller-exclusive: only the interactive controller may resize\"\n                    .to_string(),\n            );\n        }\n        session\n            .resize(SurfaceSize::new(req.rows, req.cols))\n            .map_err(|e| format!(\"resize failed: {e}\"))?;\n        recover_log(&log).set_size_and_notify(req.rows, req.cols);\n        Ok(())\n    }\n\n    /// Report the broker-owned net endpoint's status (D4a). Answered even when\n    /// the broker has no network host (`enabled: false`) so a brain can probe\n    /// capability without treating absence as an error.\n    // [impl->REQ-DAEMON-2]\n    fn dispatch_net_status(&self, send: &SharedSend) {\n        let reply = match self.net.get() {\n            Some(host) => NetStatusReply {\n                enabled: true,\n                node_id_hex: Some(host.node_id_hex()),\n                addr: host.addr_json(),\n                conn_count: host.conn_count(),\n            },\n            None => NetStatusReply {\n                enabled: false,\n                node_id_hex: None,\n                addr: serde_json::Value::Null,\n                conn_count: 0,\n            },\n        };\n        let frame = Envelope::new(\n            KIND_NET_STATUS_REPLY,\n            serde_json::to_value(reply).expect(\"NetStatusReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Report the broker's OWN compiled image version (F-025). `env!` bakes the\n    /// version at build time, so a broker that SURVIVED a `spt update apply`\n    /// (brain-only restart, ADR-0018 D3-3) honestly reports the PRE-apply version\n    /// — the authoritative running-image truth the `daemon status` version surface\n    /// compares against the freshly-installed CLI. Sourced from the running\n    /// process, never inferred from disk (the disk is the half already ahead).\n    // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n    fn dispatch_broker_image(&self, send: &SharedSend) {\n        let frame = Envelope::new(\n            KIND_BROKER_IMAGE_REPLY,\n            serde_json::to_value(BrokerImageReply {\n                // spt-daemon's CARGO_PKG_VERSION == the workspace version this\n                // broker binary was compiled at.\n                version: env!(\"CARGO_PKG_VERSION\").to_string(),\n            })\n            .expect(\"BrokerImageReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Answer the brain-subscriber stall-evict tally query (LIFECYCLE-TRUTH W2\n    /// observability rider) from the broker-global counters — so `daemon status` can\n    /// surface a brain that never returned as a diagnosable fact, not a silent absence.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn dispatch_stall_evicts(&self, send: &SharedSend) {\n        let (count, last_ms) = stall_evict_stats();\n        let frame = Envelope::new(\n            KIND_STALL_EVICTS_REPLY,\n            serde_json::to_value(StallEvictsReply { count, last_ms })\n                .expect(\"StallEvictsReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Handle a planned brain-restart request (ADR-0018 D3-3, Q3): raise the\n    /// supervisor's restart signal so it cycles the brain child onto the swapped\n    /// binary with `start-reason=update`, then ack. `honored` reflects whether a\n    /// supervisor is wired (a bare test broker has none → accepted-but-unhonored).\n    /// The broker — which observes every respawn — stamps the `update` reason,\n    /// keeping planned-vs-crash authoritative on this side (V2).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-UPD-3]\n    fn dispatch_brain_restart(&self, send: &SharedSend) {\n        let honored = match self.brain_restart.get() {\n            Some(signal) => {\n                signal.request();\n                true\n            }\n            None => false,\n        };\n        let frame = Envelope::new(\n            KIND_BRAIN_RESTARTED,\n            serde_json::to_value(BrainRestarted { honored })\n                .expect(\"BrainRestarted serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a).\n    ///\n    /// With an `op_id` the dial is routed through the broker-owned\n    /// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,\n    /// exactly like an idempotent PTY write: a replay of the same id after a\n    /// brain crash is deduped (no second connection) and answered with the\n    /// connection the first apply opened. The `conn_id` is recorded *inside*\n    /// the journaled closure, so a concurrent deduped replay always finds it.\n    /// A deduped id whose connection this process no longer holds (a broker\n    /// restart — the journal survives, connections don't) is a typed error","startLine":2910,"lineNumbers":[2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,null,3439,null,3685,null,3702,3703,null,3760,3761,3762,3763,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,3822,3823,3824,3825,3826,3827,3828,3829,3830,3831,3832,3833,3834,3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851,3852,3853,3854,3855,3856,3857,3858,3859,3860,3861,3862,3863,3864,3865,3866,3867,3868,3869,3870,3871,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885,3886,3887,3888,3889,3890,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3903,3904,3905,3906,3907,3908,3909,3910]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784195780742}}
{"type":"message","id":"1df5fcdd","parentId":"a12e95d2","timestamp":"2026-07-16T09:56:20.765Z","message":{"role":"toolResult","toolCallId":"call_gexcEeLjCwzEWYiI2JIoPOcj|fc_0cc7a0c9a797f232016a58aac490b8819aac02c66612aaa1f3","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":1784195780765}}
{"type":"message","id":"77f96310","parentId":"1df5fcdd","timestamp":"2026-07-16T09:56:20.765Z","message":{"role":"toolResult","toolCallId":"call_IJNW5R5tq7OhG1Q8ONSCHqYX|fc_0cc7a0c9a797f232016a58aac490e0819a98d06a7eb53469c7","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":1784195780765}}
{"type":"message","id":"662de627","parentId":"77f96310","timestamp":"2026-07-16T09:56:20.771Z","message":{"role":"toolResult","toolCallId":"call_YysYSqEtdnpWJYQmfX3SF5Dg|fc_0cc7a0c9a797f232016a58aac490fc819aba47ef5c3438ac2d","toolName":"read","content":[{"type":"text","text":"//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n//! onto a local session for an operator on another node.\n//!\n//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n//! [`AttachRecord::Request`]; this module is the target-side pump between that\n//! stream and the local broker-hosted session:\n//!\n//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n//!   journal (a replayed input record cannot double-type);\n//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n//!\n//! ## Exactly-once lives at the EDGES, not the wire sends\n//!\n//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n//! and *discards* everything interleaved — inside a full-duplex pump loop that\n//! eats the operator's `Input` records (both sides starve; found the hard way\n//! in `tests/attach.rs`). Idempotency holds without it because each direction\n//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n//! session-log `seq` (the render cursor). A post-restart re-serve may\n//! re-transmit output records on the wire — duplicates are dropped at the\n//! render cursor, the same discipline the local `OutputLog` replay uses\n//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n//!\n//! **Compute + files stay here**; the operator is a viewport (CONTEXT\n//! §Remote-control vs local operation). The access gate runs before anything\n//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\n//! [`access_check`] sees the **handshake-proven** origin from the broker's\n//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n//! your own running instance is ungated by default (ADR-0009 default-open);\n//! the D5d whitelist drops in at this same call.\n//!\n//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n//! A restarted target brain re-attaches the session from its output cursor,\n//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n//! a restarted operator brain resubscribes and dedups rendered chunks by\n//! `Output.seq`.\n\nuse std::io;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::ndjson;\n\nuse crate::access::{access_check, AccessDecision, InboundClass};\nuse crate::brain::{now_ms, Brain, BrokerEvent};\nuse crate::effect::{Minter, MintedOp};\nuse crate::msg::{decode_bytes, encode_bytes};\n\n/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n/// served viewport is a **driver** — attaching wakes the endpoint, the\n/// viewport ending detaches it (active → dormant; the transition echo is\n/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n/// [`mark_driven_by`]: a session served without a perch has no resting\n/// record to move.\n// [impl->REQ-INST-3]\nfn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n}\n\n/// Why a serve loop returned.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum AttachServeOutcome {\n    /// The access gate refused the requesting origin, OR a `Control` attach hit\n    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n    /// busy guidance client-side from the endpoint's `driven_by`.\n    Refused,\n    /// The session's child exited; the `Exit` record was sent and the stream\n    /// finished.\n    Exited(Option<i32>),\n    /// The operator closed its send side (detached); the viewport ended with\n    /// the session still running.\n    Detached,\n    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n    Displaced,\n}\n\n/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n/// arrives — gates wake (controller-only) and PTY input.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ServeRole {\n    /// The interactive controller (input + resize; wakes the endpoint).\n    Controller,\n    /// A read-only viewer (output only; wake-neutral).\n    Viewer,\n}\n\n/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\n/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\n/// hopelessly behind under a sustained flood will be re-evicted immediately after\n/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n/// it, the operator sees an intermittent LIVE burst each interval (forward\n/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\nconst RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n\n/// The CONTROLLER fell behind the live ring further than the ring holds — the\n/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n#[derive(Debug)]\npub struct ControllerIrrecoverablyBehind {\n    /// The frozen `delivered_through` floor the ring has already rolled past.\n    pub floor: u64,\n}\n\nimpl std::fmt::Display for ControllerIrrecoverablyBehind {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"controller fell irrecoverably behind the live ring at seq {} (dropped frames rolled out of the ring)\",\n            self.floor\n        )\n    }\n}\n\nimpl std::error::Error for ControllerIrrecoverablyBehind {}\n\n/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\n/// That is the only error the controller gap-resume recovers from; every other\n/// error propagates unchanged.\nfn is_output_gap(e: &io::Error) -> bool {\n    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n}\n\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\n/// b4 drop-don't-block output-gap instead of fataling.\n///\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\n/// bounded channel is full (a controller that falls behind its own echo under a\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\n/// resume). So instead it RE-FETCHES: re-subscribe from\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\n///\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n/// Size / Exit / …) pass through unchanged.\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\npub fn read_controller_event_resuming(\n    brain: &mut Brain,\n    session_id: u64,\n    by: Option<&str>,\n) -> io::Result<BrokerEvent> {\n    let mut last_resume_floor: Option<u64> = None;\n    let mut swallow_resume_ack = false;\n    loop {\n        match brain.read_event() {\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\n            // resolved) so it cannot mask a repeated gap as forward progress.\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\n                swallow_resume_ack = false;\n                continue;\n            }\n            Ok(ev) => return Ok(ev),\n            Err(e) if is_output_gap(&e) => {\n                let floor = brain.controller_resume_floor();\n                if last_resume_floor == Some(floor) {\n                    // No progress across two resumes → the ring rolled past the\n                    // floor → the dropped frames are gone.\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\n    remote_session: Option<u64>,\n) -> io::Result<AttachServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\n    // session output (broker→serve) — including the controller's synchronous,\n    // inline, multi-frame ring replay (`become_controller`), which holds the\n    // broker's single-threaded per-conn handler for the whole replay. If the\n    // forwards rode `brain` too, that stuck handler could not read them →\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n    // two directions on independent single-threaded handlers cannot block each\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n    // a second conn is correct. Order is preserved: this one serve thread reads\n    // every Output in seq order on `brain` and forwards it in that same order on\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n    // the controller onto that async path would remove the inline-replay block at\n    // the source, but it touches the authoritative cursor-advancing path.)\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    let mut decoder = AttachDecoder::new();\n    let mut attached = false;\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\n    // race on the input gate).\n    let mut intent = AttachIntent::Control;\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\n    // controller input that races ahead of the reply is BUFFERED, not dropped\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\n    // we know we control the session (the busy-refuse case).\n    let mut role: Option<ServeRole> = None;\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n    // The attached session — captured from the Request so the terminal paths can\n    // explicitly release this worker's role (clearing driven_by / viewer_count\n    // promptly, not only on connection drop).\n    let mut attached_session: Option<u64> = None;\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n    // intermittent live bursts, never a CPU spin.\n    let mut last_resubscribe: Option<Instant> = None;\n\n    loop {\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\n        // role resolves no Output flows, so a plain read is correct then.\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n        let event = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\n                    Ok(ev) => ev,\n                    Err(e)\n                        if e.get_ref()\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\n                    {\n                        // Irrecoverable: the dropped frames rolled out of the ring.\n                        // SURFACE a marked truncation to the operator (never a\n                        // silent skip = B2 lie, never a spin), then end cleanly.\n                        // Graceful continuation is deferred\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\n                        let notice =\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n                        let line = ndjson::encode_line(&AttachRecord::Output {\n                            seq: brain.controller_resume_floor(),\n                            data_b64: encode_bytes(notice),\n                        });\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\n                            // the transport-proven origin. Viewing is gated\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\n                            // watching reveals full session contents).\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\n                                == AccessDecision::Deny\n                            {\n                                wire.net_stream_send(stream_id, &[], None, true)?;\n                                return Ok(AttachServeOutcome::Refused);\n                            }\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\n                            // dispatcher-resolved session id (the owning node is\n                            // authoritative for its own table); the LOCAL path uses\n                            // the wire session_id unchanged.\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\n                            let sid = remote_session.unwrap_or(session_id);\n                            intent = req_intent;\n                            attached_session = Some(sid);\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\n                            // resolves controller / viewer / busy / take by the\n                            // intent + this handshake-proven origin, and replies a\n                            // `Subscribed`. driven_by / viewer_count are now\n                            // BROKER-owned (single writer — resolves the\n                            // clear-race), so this loop no longer stamps them.\n                            // A VIEWER attach ARMS snap-above at the floor so a\n                            // PRE-eviction ring-roll forward Output gap (the ring\n                            // rolled frames out under flood with NO eviction\n                            // marker) SNAPS to live instead of fataling the\n                            // legacy reject-gap → the viewer keeps forwarding.\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\n                            // resume, B2). Composes with the POST-eviction\n                            // ViewerEvicted skip-to-live arm below.\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n                            if req_intent == AttachIntent::Viewer {\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\n                            } else {\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n                            }\n                            attached = true;\n                        }\n                        AttachRecord::Input { data_b64, op_id }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            match role {\n                                // Confirmed controller — exactly-once at the PTY\n                                // (the operator-minted id rides the B5 journal). NO-ACK\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\n                                // fire-and-forward (the ack is discarded here) and can\n                                // flood N input frames on this one conn; an acked write\n                                // back would fill the return direction and deadlock the\n                                // broker's per-conn handler. Dedup still holds at the\n                                // broker (op_id applied-set).\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n                                Some(ServeRole::Controller) => {\n                                    // The op_id is operator-minted (rc attach OpMinter),\n                                    // so it carries the `rc` journal tag (ADR-0034).\n                                    brain.send_effect_no_ack(\n                                        MintedOp::new(Minter::Rc, op_id),\n                                        &bytes,\n                                    )?\n                                }\n                                // Outcome not yet known — buffer; flushed on a\n                                // controller confirmation, discarded on busy/viewer.\n                                None => pending_input.push((op_id, bytes)),\n                                // A viewer never reaches here (intent gate).\n                                Some(ServeRole::Viewer) => {}\n                            }\n                        }\n                        AttachRecord::Resize { rows, cols }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\n                            // broker rejects it on a viewer's stream). A viewer's\n                            // Resize is dropped here by the intent gate too.\n                            let _ = brain.resize(rows, cols);\n                        }\n                        // Input/Resize before Request, a viewer's input/resize,\n                        // or target-direction records echoed back: noise — ignore.\n                        _ => {}\n                    }\n                }\n            }\n\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\n            BrokerEvent::Subscribed { outcome, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\n                        // Flush any input that raced ahead of the reply (operator-\n                        // minted ops carry the `rc` tag).\n                        for (op, b) in pending_input.drain(..) {\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\n                        }\n                    }\n                    O::Viewer => {\n                        role = Some(ServeRole::Viewer);\n                        // Wake-neutral: a viewer never wakes the endpoint, never\n                        // keeps it awake (Gap B). Discard any racing input.\n                        pending_input.clear();\n                    }\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n…\n        }\n    }\n}\n\n[Showing lines 1-458 of 627. Use :459 to continue]"}],"details":{"truncation":{"content":"//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\r\n//! onto a local session for an operator on another node.\r\n//!\r\n//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\r\n//! [`AttachRecord::Request`]; this module is the target-side pump between that\r\n//! stream and the local broker-hosted session:\r\n//!\r\n//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\r\n//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\r\n//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\r\n//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\r\n//!   journal (a replayed input record cannot double-type);\r\n//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\r\n//!\r\n//! ## Exactly-once lives at the EDGES, not the wire sends\r\n//!\r\n//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\r\n//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\r\n//! and *discards* everything interleaved — inside a full-duplex pump loop that\r\n//! eats the operator's `Input` records (both sides starve; found the hard way\r\n//! in `tests/attach.rs`). Idempotency holds without it because each direction\r\n//! dedups at its **effect**: the PTY write is journaled by the operator-minted\r\n//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\r\n//! session-log `seq` (the render cursor). A post-restart re-serve may\r\n//! re-transmit output records on the wire — duplicates are dropped at the\r\n//! render cursor, the same discipline the local `OutputLog` replay uses\r\n//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\r\n//!\r\n//! **Compute + files stay here**; the operator is a viewport (CONTEXT\r\n//! §Remote-control vs local operation). The access gate runs before anything\r\n//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\r\n//! [`access_check`] sees the **handshake-proven** origin from the broker's\r\n//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\r\n//! your own running instance is ungated by default (ADR-0009 default-open);\r\n//! the D5d whitelist drops in at this same call.\r\n//!\r\n//! Restart survival composes from D4/B5 with **no new machinery**: the broker\r\n//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\r\n//! A restarted target brain re-attaches the session from its output cursor,\r\n//! resubscribes the stream from its net cursor, and re-drives journaled sends;\r\n//! a restarted operator brain resubscribes and dedups rendered chunks by\r\n//! `Output.seq`.\r\n\r\nuse std::io;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\r\nuse spt_net::net::ndjson;\r\n\r\nuse crate::access::{access_check, AccessDecision, InboundClass};\r\nuse crate::brain::{now_ms, Brain, BrokerEvent};\r\nuse crate::effect::{Minter, MintedOp};\r\nuse crate::msg::{decode_bytes, encode_bytes};\r\n\r\n/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\r\n/// served viewport is a **driver** — attaching wakes the endpoint, the\r\n/// viewport ending detaches it (active → dormant; the transition echo is\r\n/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\r\n/// [`mark_driven_by`]: a session served without a perch has no resting\r\n/// record to move.\r\n// [impl->REQ-INST-3]\r\nfn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\r\n    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\r\n}\r\n\r\n/// Why a serve loop returned.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub enum AttachServeOutcome {\r\n    /// The access gate refused the requesting origin, OR a `Control` attach hit\r\n    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\r\n    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\r\n    /// busy guidance client-side from the endpoint's `driven_by`.\r\n    Refused,\r\n    /// The session's child exited; the `Exit` record was sent and the stream\r\n    /// finished.\r\n    Exited(Option<i32>),\r\n    /// The operator closed its send side (detached); the viewport ended with\r\n    /// the session still running.\r\n    Detached,\r\n    /// This controller was DISPLACED by a different operator's `--take`/`kick`\r\n    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\r\n    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\r\n    Displaced,\r\n}\r\n\r\n/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\r\n/// arrives — gates wake (controller-only) and PTY input.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum ServeRole {\r\n    /// The interactive controller (input + resize; wakes the endpoint).\r\n    Controller,\r\n    /// A read-only viewer (output only; wake-neutral).\r\n    Viewer,\r\n}\r\n\r\n/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\r\n/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\r\n/// hopelessly behind under a sustained flood will be re-evicted immediately after\r\n/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\r\n/// it, the operator sees an intermittent LIVE burst each interval (forward\r\n/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\r\nconst RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\r\n\r\n/// The CONTROLLER fell behind the live ring further than the ring holds — the\r\n/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\r\n/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\r\n/// an ordinary fatal so the serve loop can surface it (a marked truncation),\r\n/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\r\n/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n#[derive(Debug)]\r\npub struct ControllerIrrecoverablyBehind {\r\n    /// The frozen `delivered_through` floor the ring has already rolled past.\r\n    pub floor: u64,\r\n}\r\n\r\nimpl std::fmt::Display for ControllerIrrecoverablyBehind {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        write!(\r\n            f,\r\n            \"controller fell irrecoverably behind the live ring at seq {} (dropped frames rolled out of the ring)\",\r\n            self.floor\r\n        )\r\n    }\r\n}\r\n\r\nimpl std::error::Error for ControllerIrrecoverablyBehind {}\r\n\r\n/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\r\n/// That is the only error the controller gap-resume recovers from; every other\r\n/// error propagates unchanged.\r\nfn is_output_gap(e: &io::Error) -> bool {\r\n    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\r\n}\r\n\r\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\r\n/// b4 drop-don't-block output-gap instead of fataling.\r\n///\r\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\r\n/// bounded channel is full (a controller that falls behind its own echo under a\r\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\r\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\r\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\r\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\r\n/// resume). So instead it RE-FETCHES: re-subscribe from\r\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\r\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\r\n///\r\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\r\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\r\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\r\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\r\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\r\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\r\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\r\n/// Size / Exit / …) pass through unchanged.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\npub fn read_controller_event_resuming(\r\n    brain: &mut Brain,\r\n    session_id: u64,\r\n    by: Option<&str>,\r\n) -> io::Result<BrokerEvent> {\r\n    let mut last_resume_floor: Option<u64> = None;\r\n    let mut swallow_resume_ack = false;\r\n    loop {\r\n        match brain.read_event() {\r\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\r\n            // resolved) so it cannot mask a repeated gap as forward progress.\r\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\r\n                swallow_resume_ack = false;\r\n                continue;\r\n            }\r\n            Ok(ev) => return Ok(ev),\r\n            Err(e) if is_output_gap(&e) => {\r\n                let floor = brain.controller_resume_floor();\r\n                if last_resume_floor == Some(floor) {\r\n                    // No progress across two resumes → the ring rolled past the\r\n                    // floor → the dropped frames are gone.\r\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\r\n                }\r\n                last_resume_floor = Some(floor);\r\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\r\n                // a silent re-take, no Displaced); the broker replays the dropped\r\n                // frames from the ring.\r\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\r\n                swallow_resume_ack = true;\r\n            }\r\n            Err(e) => return Err(e),\r\n        }\r\n    }\r\n}\r\n\r\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\r\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\r\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\r\n/// its own session table, so a remote operator — which cannot know our local\r\n/// session id — names the endpoint and we map it here, never trusting a wire\r\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\r\n/// (a stale registry row routed us an attach for something gone → the caller\r\n/// refuses cleanly, D6).\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\r\n    brain.sessions().ok().and_then(|reply| {\r\n        reply\r\n            .sessions\r\n            .into_iter()\r\n            .find(|s| s.endpoint == endpoint)\r\n            .map(|s| s.session_id)\r\n    })\r\n}\r\n\r\n/// Serve one attach stream against this brain's broker, blocking until the\r\n/// session exits, the operator detaches, or the gate refuses.\r\n///\r\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\r\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\r\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\r\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\r\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\r\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\r\n/// journal, and re-pumped output dedups at the operator's render cursor).\r\n///\r\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\r\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\r\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\r\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\r\n/// round-trip inside the event loop would swallow interleaved attach records\r\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\r\n/// the wire `session_id`.\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n                        // Irrecoverable: the dropped frames rolled out of the ring.\r\n                        // SURFACE a marked truncation to the operator (never a\r\n                        // silent skip = B2 lie, never a spin), then end cleanly.\r\n                        // Graceful continuation is deferred\r\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\r\n                        let notice =\r\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\r\n                        let line = ndjson::encode_line(&AttachRecord::Output {\r\n                            seq: brain.controller_resume_floor(),\r\n                            data_b64: encode_bytes(notice),\r\n                        });\r\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                    Err(e) => {\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                }\r\n            }\r\n            _ => brain.read_event()?,\r\n        };\r\n        match event {\r\n            // ── Wire → session: requests, input, resize. ───────────────────\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r","truncated":true,"truncatedBy":"lines","totalLines":627,"totalBytes":26174,"outputLines":458,"outputBytes":26174,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\r\n//! onto a local session for an operator on another node.\r\n//!\r\n//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\r\n//! [`AttachRecord::Request`]; this module is the target-side pump between that\r\n//! stream and the local broker-hosted session:\r\n//!\r\n//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\r\n//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\r\n//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\r\n//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\r\n//!   journal (a replayed input record cannot double-type);\r\n//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\r\n//!\r\n//! ## Exactly-once lives at the EDGES, not the wire sends\r\n//!\r\n//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\r\n//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\r\n//! and *discards* everything interleaved — inside a full-duplex pump loop that\r\n//! eats the operator's `Input` records (both sides starve; found the hard way\r\n//! in `tests/attach.rs`). Idempotency holds without it because each direction\r\n//! dedups at its **effect**: the PTY write is journaled by the operator-minted\r\n//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\r\n//! session-log `seq` (the render cursor). A post-restart re-serve may\r\n//! re-transmit output records on the wire — duplicates are dropped at the\r\n//! render cursor, the same discipline the local `OutputLog` replay uses\r\n//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\r\n//!\r\n//! **Compute + files stay here**; the operator is a viewport (CONTEXT\r\n//! §Remote-control vs local operation). The access gate runs before anything\r\n//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\r\n//! [`access_check`] sees the **handshake-proven** origin from the broker's\r\n//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\r\n//! your own running instance is ungated by default (ADR-0009 default-open);\r\n//! the D5d whitelist drops in at this same call.\r\n//!\r\n//! Restart survival composes from D4/B5 with **no new machinery**: the broker\r\n//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\r\n//! A restarted target brain re-attaches the session from its output cursor,\r\n//! resubscribes the stream from its net cursor, and re-drives journaled sends;\r\n//! a restarted operator brain resubscribes and dedups rendered chunks by\r\n//! `Output.seq`.\r\n\r\nuse std::io;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\r\nuse spt_net::net::ndjson;\r\n\r\nuse crate::access::{access_check, AccessDecision, InboundClass};\r\nuse crate::brain::{now_ms, Brain, BrokerEvent};\r\nuse crate::effect::{Minter, MintedOp};\r\nuse crate::msg::{decode_bytes, encode_bytes};\r\n\r\n/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\r\n/// served viewport is a **driver** — attaching wakes the endpoint, the\r\n/// viewport ending detaches it (active → dormant; the transition echo is\r\n/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\r\n/// [`mark_driven_by`]: a session served without a perch has no resting\r\n/// record to move.\r\n// [impl->REQ-INST-3]\r\nfn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\r\n    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\r\n}\r\n\r\n/// Why a serve loop returned.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub enum AttachServeOutcome {\r\n    /// The access gate refused the requesting origin, OR a `Control` attach hit\r\n    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\r\n    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\r\n    /// busy guidance client-side from the endpoint's `driven_by`.\r\n    Refused,\r\n    /// The session's child exited; the `Exit` record was sent and the stream\r\n    /// finished.\r\n    Exited(Option<i32>),\r\n    /// The operator closed its send side (detached); the viewport ended with\r\n    /// the session still running.\r\n    Detached,\r\n    /// This controller was DISPLACED by a different operator's `--take`/`kick`\r\n    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\r\n    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\r\n    Displaced,\r\n}\r\n\r\n/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\r\n/// arrives — gates wake (controller-only) and PTY input.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum ServeRole {\r\n    /// The interactive controller (input + resize; wakes the endpoint).\r\n    Controller,\r\n    /// A read-only viewer (output only; wake-neutral).\r\n    Viewer,\r\n}\r\n\r\n/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\r\n/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\r\n/// hopelessly behind under a sustained flood will be re-evicted immediately after\r\n/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\r\n/// it, the operator sees an intermittent LIVE burst each interval (forward\r\n/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\r\nconst RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\r\n\r\n/// The CONTROLLER fell behind the live ring further than the ring holds — the\r\n/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\r\n/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\r\n/// an ordinary fatal so the serve loop can surface it (a marked truncation),\r\n/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\r\n/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n#[derive(Debug)]\r\npub struct ControllerIrrecoverablyBehind {\r\n    /// The frozen `delivered_through` floor the ring has already rolled past.\r\n    pub floor: u64,\r\n}\r\n\r\nimpl std::fmt::Display for ControllerIrrecoverablyBehind {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        write!(\r\n            f,\r\n            \"controller fell irrecoverably behind the live ring at seq {} (dropped frames rolled out of the ring)\",\r\n            self.floor\r\n        )\r\n    }\r\n}\r\n\r\nimpl std::error::Error for ControllerIrrecoverablyBehind {}\r\n\r\n/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\r\n/// That is the only error the controller gap-resume recovers from; every other\r\n/// error propagates unchanged.\r\nfn is_output_gap(e: &io::Error) -> bool {\r\n    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\r\n}\r\n\r\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\r\n/// b4 drop-don't-block output-gap instead of fataling.\r\n///\r\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\r\n/// bounded channel is full (a controller that falls behind its own echo under a\r\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\r\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\r\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\r\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\r\n/// resume). So instead it RE-FETCHES: re-subscribe from\r\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\r\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\r\n///\r\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\r\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\r\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\r\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\r\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\r\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\r\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\r\n/// Size / Exit / …) pass through unchanged.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\npub fn read_controller_event_resuming(\r\n    brain: &mut Brain,\r\n    session_id: u64,\r\n    by: Option<&str>,\r\n) -> io::Result<BrokerEvent> {\r\n    let mut last_resume_floor: Option<u64> = None;\r\n    let mut swallow_resume_ack = false;\r\n    loop {\r\n        match brain.read_event() {\r\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\r\n            // resolved) so it cannot mask a repeated gap as forward progress.\r\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\r\n                swallow_resume_ack = false;\r\n                continue;\r\n            }\r\n            Ok(ev) => return Ok(ev),\r\n            Err(e) if is_output_gap(&e) => {\r\n                let floor = brain.controller_resume_floor();\r\n                if last_resume_floor == Some(floor) {\r\n                    // No progress across two resumes → the ring rolled past the\r\n                    // floor → the dropped frames are gone.\r\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\r\n                }\r\n                last_resume_floor = Some(floor);\r\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\r\n                // a silent re-take, no Displaced); the broker replays the dropped\r\n                // frames from the ring.\r\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\r\n                swallow_resume_ack = true;\r\n            }\r\n            Err(e) => return Err(e),\r\n        }\r\n    }\r\n}\r\n\r\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\r\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\r\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\r\n/// its own session table, so a remote operator — which cannot know our local\r\n/// session id — names the endpoint and we map it here, never trusting a wire\r\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\r\n/// (a stale registry row routed us an attach for something gone → the caller\r\n/// refuses cleanly, D6).\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\r\n    brain.sessions().ok().and_then(|reply| {\r\n        reply\r\n            .sessions\r\n            .into_iter()\r\n            .find(|s| s.endpoint == endpoint)\r\n            .map(|s| s.session_id)\r\n    })\r\n}\r\n\r\n/// Serve one attach stream against this brain's broker, blocking until the\r\n/// session exits, the operator detaches, or the gate refuses.\r\n///\r\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\r\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\r\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\r\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\r\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\r\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\r\n/// journal, and re-pumped output dedups at the operator's render cursor).\r\n///\r\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\r\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\r\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\r\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\r\n/// round-trip inside the event loop would swallow interleaved attach records\r\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\r\n/// the wire `session_id`.\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n                        // Irrecoverable: the dropped frames rolled out of the ring.\r\n                        // SURFACE a marked truncation to the operator (never a\r\n                        // silent skip = B2 lie, never a spin), then end cleanly.\r\n                        // Graceful continuation is deferred\r\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\r\n                        let notice =\r\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\r\n                        let line = ndjson::encode_line(&AttachRecord::Output {\r\n                            seq: brain.controller_resume_floor(),\r\n                            data_b64: encode_bytes(notice),\r\n                        });\r\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                    Err(e) => {\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                }\r\n            }\r\n            _ => brain.read_event()?,\r\n        };\r\n        match event {\r\n            // ── Wire → session: requests, input, resize. ───────────────────\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n…\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,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":26174,"outputLines":458,"outputBytes":26174,"shownRange":{"start":1,"end":458},"nextOffset":459}}},"isError":false,"timestamp":1784195780771}}
{"type":"message","id":"eb1a3c20","parentId":"662de627","timestamp":"2026-07-16T09:56:36.267Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing update stall causes**"},{"type":"toolCall","id":"call_8T09JSC1XIofMGXmo14ZmMxm|fc_0cc7a0c9a797f232016a58aad43520819a81477da78686c988","name":"grep","arguments":{"i":"Finding wedge hazard contracts","pattern":"UPDATE-WEDGE|BRAIN-RESUME-NO-CONN-DEADLOCK|BROKER-VIEWER-BRAIN-DECOUPLE|SHAREDSEND-NO-BLOCKING|UPDATE-PROMOTE-DRAINED|update wedge|brain conn","path":"docs/KNOWN-HAZARDS.md;docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md;CHANGELOG.md;CONTEXT.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_f0GzbkXGxaHIVQoe0fsCdXcj|fc_0cc7a0c9a797f232016a58aad43530819aa39affcac0a8b54f","name":"grep","arguments":{"i":"Finding current resume implementation","pattern":"resume_session_cursors|resume_sessions|any_local_controller_wedged|old_gen_drained|write_ready|net_status\\(\\)|sessions\\(\\)","path":"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/false_promote.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_blKHb6boCJi4PBFA7AT1S1CV|fc_0cc7a0c9a797f232016a58aad43538819abba8742aa61d0058","name":"grep","arguments":{"i":"Finding connection poison implementation","pattern":"struct BrokerConn|write_gated|write\\(|poison|deadline|retire|cancel","path":"crates/spt-daemon/src/conn.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_pNsUeuhVUw4Lzy0omddKVAUY|fc_0cc7a0c9a797f232016a58aad4353c819ab980f3a6a89e84fc","name":"read","arguments":{"i":"Reading lifecycle truth triage","path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","selector":null}},{"type":"toolCall","id":"call_eHn52OXWtLcpK1oTsIZFEtd0|fc_0cc7a0c9a797f232016a58aad43540819abefc85398241e059","name":"read","arguments":{"i":"Reading current brain heartbeat","path":"crates/spt-daemon/src/brainproc.rs","selector":"155-335,1080-1145"}},{"type":"toolCall","id":"call_OsK1ILYh2s4Z0hlJd9KXbQBc|fc_0cc7a0c9a797f232016a58aad43548819a8a33a750f11da255","name":"read","arguments":{"i":"Reading current resume methods","path":"crates/spt-daemon/src/brain.rs","selector":"430-585"}},{"type":"toolCall","id":"call_kPWGuYDO30jMFCFJVNYBqzDK|fc_0cc7a0c9a797f232016a58aad4354c819ab0ba6a9096897478","name":"read","arguments":{"i":"Reading bounded connection writes","path":"crates/spt-daemon/src/conn.rs","selector":"1-330"}},{"type":"toolCall","id":"call_MNNDN4XsjyfnCRvWfz2rs47C|fc_0cc7a0c9a797f232016a58aad43550819a932ba29614ccb037","name":"read","arguments":{"i":"Reading resume deadlock test","path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","selector":"1-360"}},{"type":"toolCall","id":"call_pbxWzMwgk5wGSgnhsrlvg14c|fc_0cc7a0c9a797f232016a58aad43558819aae02ba74599c9635","name":"read","arguments":{"i":"Reading false promotion test","path":"crates/spt-daemon/tests/false_promote.rs","selector":"1-340"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":40149,"output":674,"cacheRead":160256,"cacheWrite":0,"totalTokens":201079,"reasoningTokens":102,"cost":{"input":0.200745,"output":0.020220000000000002,"cacheRead":0.08012799999999999,"cacheWrite":0,"total":0.301093}},"stopReason":"toolUse","timestamp":1784195780808,"responseId":"resp_0cc7a0c9a797f232016a58aac534d0819aa48467da76b3c0f9","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cc7a0c9a797f232016a58aac80a5c819ab84e0f93430e72fe","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrU_2oOKy6sNqURzbgl5rjQ57vlaxJwGXAjUR_8L0l2WbzXBTsJBPTHRVC2aBdq3BoK3TvwcwRlMFJ8yfE8mLc9-nfMcSnm0PE03m9RvRr_0qpsboBYgh5JzuR6VMP6huIgIPYBGl2EzLvLlGyOyEdDsH2s7cyJfq6drZV-AHlgtt-mEtMciKsL549IIoXklDVn0h9euWPaThCn8n7NsiPAcJyMQ54syiqZ0CMjFBDWnnHi8TRE-gDvaGSNzAg4lfdO1X_tYHDwBxHthblZHIPg3Jc0AMbIr7lSOpQE7OFn5WaK7No7jSPJRsxpqUsS6u9vCwTtULW-B5srQjgx0XaqElgDIjTLCsJFE8_wi_d7N9IdKwLNVPM6PTELWTWp1yEAw9RTuA1yFva3xfCvroPefUTlA3btHpPjUt_jRK2NjcsjitX2lIrCSfZhh1NCE7fmxD5ZsYkrlwZqrjNSLIbC_S8K084ZPJgzuxEILSPcKWJLcFqx5uyN0NwqytzaiR5f71xTrNgY3vZOpL8v6D4gI7Hpuxk1yVNj7R9TR_yYD092uOAamG_Y2LbmJ75UOfmc4j1PWip8BK3Wv2E5jLR1gY8k_MmA4XNRxsN2HZWjB4TJWUFZKrux8V1TF48XCI52k1SfjqS_osczUyprkTVZ3OdbgnF9QUjjFmxuYUw5mSS7gdMXfA-UauoEa5sju_c_yny9vZoyVIMkKXTlv_twCQhpwvCwwNOyQhZkniEtg3eSBkCrGp9KhDlZPn0a6b_PvqbENIHlOe0cMCmeg2iVS_8TK-jg8kMKOa46Cm0pPYJJ2caeeTAmcOUitQVyWXuKtpgQSouhdFWQYsGVA1f6FQNPcD_xokxzHGdqKL47Y0TB82cKWk-b0KHCDuJmORQW5U5rQoXvy0dbf0suaHpKImuvRJWYcg3C08EEqP7CIQitO7JpwK_3QiLoGecWGOf34MYzcIQk81NA4JvZuGyFZSJBKlJEnF4hRZZMPQjEgspcq6r_MTueMsK6DCTaw_n5qGldDdKTTIjwT3IHNMAc1-rwOHabki6giEnb0c9gcjByDR2fjrw4-qilaNcpFXaeHYVE8qWd_WPLiZJM4MH1F2D-ZTPnfLxS_fpJxU1T1bxKcqDUNT641t3okHGNyem7kMtyvxPjGu1w5Kbu6wn5DT0j1DkJ7Ix3eEQfNIe8OE1kqRzIUg2aQOE3WdDwjVrPvyBZF_fy8x7Lz9HKDHLG6ygqFNzbbAj8fSPdbY1egIS_P6gi6sY1aRoQzRVl5ent1xvXTTqrMfoewHHcVch3wF515kub-Om2emsdF1f2YseiqXEkgnadpycLtWaBb6ZC-SVCi9qFDWdTkwzkxdh5J_at991wnMH8__jvKxycM2G3ikmOKuFrwmi1qBmjKIGrADFcl_iz0pc3viqAEjwoUOj4R4C6ZE8XMoy3DCrJ9ELlV79_dkMfpRDiHZ_HZ6QUwRbkm7A8boZYMnkgfl7f8IDtjI29dEgme4-NkNFfRzYwJB4-09chFt_X4BYDwfI1kngiFuw3pSdDyKX5QBHXPZuYRQach0baYdYKVA4cbPi4KJdJF7kqsKxgH9IcQBER","summary":[{"type":"summary_text","text":"**Assessing update stall causes**"}]},{"id":"fc_0cc7a0c9a797f232016a58aad43520819a81477da78686c988","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding wedge hazard contracts\",\"pattern\":\"UPDATE-WEDGE|BRAIN-RESUME-NO-CONN-DEADLOCK|BROKER-VIEWER-BRAIN-DECOUPLE|SHAREDSEND-NO-BLOCKING|UPDATE-PROMOTE-DRAINED|update wedge|brain conn\",\"path\":\"docs/KNOWN-HAZARDS.md;docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md;CHANGELOG.md;CONTEXT.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_8T09JSC1XIofMGXmo14ZmMxm","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aad43530819aa39affcac0a8b54f","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding current resume implementation\",\"pattern\":\"resume_session_cursors|resume_sessions|any_local_controller_wedged|old_gen_drained|write_ready|net_status\\\\(\\\\)|sessions\\\\(\\\\)\",\"path\":\"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/false_promote.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_f0GzbkXGxaHIVQoe0fsCdXcj","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aad43538819abba8742aa61d0058","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding connection poison implementation\",\"pattern\":\"struct BrokerConn|write_gated|write\\\\(|poison|deadline|retire|cancel\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_blKHb6boCJi4PBFA7AT1S1CV","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aad4353c819ab980f3a6a89e84fc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading lifecycle truth triage\",\"path\":\"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\",\"selector\":null}","call_id":"call_pNsUeuhVUw4Lzy0omddKVAUY","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aad43540819abefc85398241e059","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current brain heartbeat\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"155-335,1080-1145\"}","call_id":"call_eHn52OXWtLcpK1oTsIZFEtd0","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aad43548819a8a33a750f11da255","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current resume methods\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"430-585\"}","call_id":"call_OsK1ILYh2s4Z0hlJd9KXbQBc","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aad4354c819ab0ba6a9096897478","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading bounded connection writes\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"1-330\"}","call_id":"call_kPWGuYDO30jMFCFJVNYBqzDK","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aad43550819a932ba29614ccb037","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading resume deadlock test\",\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"selector\":\"1-360\"}","call_id":"call_MNNDN4XsjyfnCRvWfz2rs47C","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aad43558819aae02ba74599c9635","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading false promotion test\",\"path\":\"crates/spt-daemon/tests/false_promote.rs\",\"selector\":\"1-340\"}","call_id":"call_pbxWzMwgk5wGSgnhsrlvg14c","name":"read"}]},"duration":15457.69560000021,"ttft":3213.4274000003934,"contextSnapshot":{"promptTokens":200405,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8T09JSC1XIofMGXmo14ZmMxm|fc_0cc7a0c9a797f232016a58aad43520819a81477da78686c988","toolName":"grep","startedAt":"2026-07-16T09:56:36.267Z","args":{"path":"docs/KNOWN-HAZARDS.md;docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md;CHANGELOG.md;CONTEXT.md"},"intent":"Finding wedge hazard contracts"},"id":"18189cc9","parentId":"eb1a3c20","timestamp":"2026-07-16T09:56:36.267Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_f0GzbkXGxaHIVQoe0fsCdXcj|fc_0cc7a0c9a797f232016a58aad43530819aa39affcac0a8b54f","toolName":"grep","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/false_promote.rs"},"intent":"Finding current resume implementation"},"id":"03250bbb","parentId":"18189cc9","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_blKHb6boCJi4PBFA7AT1S1CV|fc_0cc7a0c9a797f232016a58aad43538819abba8742aa61d0058","toolName":"grep","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Finding connection poison implementation"},"id":"ef5140bd","parentId":"03250bbb","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pNsUeuhVUw4Lzy0omddKVAUY|fc_0cc7a0c9a797f232016a58aad4353c819ab980f3a6a89e84fc","toolName":"read","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md"},"intent":"Reading lifecycle truth triage"},"id":"143e0163","parentId":"ef5140bd","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eHn52OXWtLcpK1oTsIZFEtd0|fc_0cc7a0c9a797f232016a58aad43540819abefc85398241e059","toolName":"read","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading current brain heartbeat"},"id":"429480a7","parentId":"143e0163","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OsK1ILYh2s4Z0hlJd9KXbQBc|fc_0cc7a0c9a797f232016a58aad43548819a8a33a750f11da255","toolName":"read","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading current resume methods"},"id":"770b06ff","parentId":"429480a7","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kPWGuYDO30jMFCFJVNYBqzDK|fc_0cc7a0c9a797f232016a58aad4354c819ab0ba6a9096897478","toolName":"read","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading bounded connection writes"},"id":"17515e7b","parentId":"770b06ff","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MNNDN4XsjyfnCRvWfz2rs47C|fc_0cc7a0c9a797f232016a58aad43550819a932ba29614ccb037","toolName":"read","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs"},"intent":"Reading resume deadlock test"},"id":"d08dbd50","parentId":"17515e7b","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pbxWzMwgk5wGSgnhsrlvg14c|fc_0cc7a0c9a797f232016a58aad43558819aae02ba74599c9635","toolName":"read","startedAt":"2026-07-16T09:56:36.268Z","args":{"path":"crates/spt-daemon/tests/false_promote.rs"},"intent":"Reading false promotion test"},"id":"94b2dc48","parentId":"d08dbd50","timestamp":"2026-07-16T09:56:36.268Z"}
{"type":"message","id":"74a35f03","parentId":"94b2dc48","timestamp":"2026-07-16T09:56:36.273Z","message":{"role":"toolResult","toolCallId":"call_blKHb6boCJi4PBFA7AT1S1CV|fc_0cc7a0c9a797f232016a58aad43538819abba8742aa61d0058","toolName":"grep","content":[{"type":"text","text":"*1|//! Bounded, cancelable, poison-on-failure framed writes for one physical\n 2|//! broker connection (UPDATE-WEDGE round-4).\n 3|//!\n 4|//! ## The failure class this kills\n...\n 12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n*13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n 16|//! brain restart).\n...\n 23|//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n*24|//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n 25|//!    write);\n 26|//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n*27|//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n 28|//! 3. on success, returns the half to the gate.\n 29|//!\n*30|//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n*31|//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n 32|//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n 33|//! completion APC makes the wedged `SleepEx` loop return with\n 34|//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n*35|//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n 36|//! the same abort (the stuck op it waited behind is the wedge).\n 37|//!\n*38|//! ## Poison / retire invariant (both OSes)\n 39|//!\n*40|//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n*41|//! connection is poisoned, idempotently — a length-prefixed frame may have\n 42|//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n 43|//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n*44|//! (3) the buffer of the canceled op is released only after the op reports\n 45|//! completion (`write_frame` returns only after the completion APC — the\n*46|//! synchronous return IS the completion handshake); (4) the retired half is\n 47|//! parked in a graveyard slot — never dropped while the watchdog might still\n 48|//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n 49|//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n...\n 53|//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n*54|//! cancel→disconnect window.\n 55|//!\n 56|//! No new output queue (the bounded per-sink queues + isolated writer threads\n 57|//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n 58|//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n*59|//! healthy connections; only the poison/retire path is new.\n 60|// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n 61|\n 62|use std::io;\n...\n 114|\n*115|/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n 116|#[derive(Debug, PartialEq, Eq)]\n 117|pub(crate) enum ConnWrite {\n 118|    /// The frame was fully written.\n...\n 125|/// The serialized write gate. `half: Some` = free; `None` = a write is in\n*126|/// flight (checked out) or the conn has retired (then `poisoned` is set).\n 127|struct Gate {\n 128|    half: Option<SendHalf>,\n 129|}\n 130|\n*131|/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\n 132|struct Dog {\n*133|    /// `Some((op_id, deadline))` while a write is in flight.\n 134|    inflight: Option<(u64, Instant)>,\n 135|    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136|    shutdown: bool,\n...\n 152|    dog_cv: Condvar,\n*153|    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n 154|    /// conn refuses every future write and is never re-gated.\n*155|    poisoned: AtomicBool,\n*156|    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n 157|    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n 158|    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n 159|    /// count the wedge (the W2 stall-evict tally) without conflating it with\n...\n 163|    severed: AtomicBool,\n*164|    /// A retired (poisoned) half parks here so its OS handle stays alive\n 165|    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n 166|    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n 167|    graveyard: Mutex<Option<SendHalf>>,\n*168|    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n 169|    op_seq: AtomicU64,\n 170|    /// The absolute per-write bound (gate-wait + OS write completion).\n 171|    bound: Duration,\n...\n 228|\n*229|    /// Render the once-per-conn retirement record — the F-039 token split\n*230|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233|    /// already-dead counterpart, the offline-peer dial churn) emits the\n 234|    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n*235|    /// SAME attribution fields — quieter class, never thinner (the retired\n 236|    /// class is what discriminates fresh-carrier churn from interleave\n*237|    /// artifact in the field). Behavior is identical either way: poison,\n*238|    /// abort, retire, never reuse.\n 239|    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n*240|    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n 241|        if timed_out {\n 242|            format!(\n*243|                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 244|                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 245|                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 246|                self.attribution(),\n...\n 255|                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n*256|                 broker conn poisoned + retired, never reused (same retire behavior \\\n 257|                 as POISONED — only the failure class differs) \\\n 258|                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n 259|                self.attribution()\n...\n 267|    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n*268|    /// timeout/cancel leg is the retirement record itself.\n 269|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 270|    fn lifecycle(&self, event: &str, extra: &str) {\n 271|        let sep = if extra.is_empty() { \"\" } else { \" \" };\n...\n 278|    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n*279|    /// the halves share one OS object) and latch the poison. Idempotent and\n 280|    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n*281|    /// no-op, and re-cancel closes the race where a write was issued a beat\n*282|    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n*283|    /// `cause` (the organic caller's write error) rides the retirement record\n*284|    /// when the retirement is NOT deadline-driven.\n*285|    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n*286|        if !self.poisoned.swap(true, Ordering::AcqRel) {\n*287|            // First poison of this conn: loud, once. The token is class-split\n*288|            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n 289|            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 290|            eprintln!(\n 291|                \"{}\",\n*292|                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n 293|            );\n 294|        }\n 295|        #[cfg(windows)]\n...\n 307|        }\n*308|        // Wake gate-waiters so they observe the poison instead of sleeping\n*309|        // out their full deadline, and the watchdog so it re-evaluates.\n 310|        self.gate_cv.notify_all();\n 311|        self.dog_cv.notify_all();\n 312|    }\n 313|\n*314|    /// Sever the physical connection (exactly once), AFTER the canceled op\n 315|    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n 316|    /// client off (its next access fails), makes a re-issued server-side\n 317|    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n*318|    /// new read in after the cancel), and defuses the drop-limbo flush; the\n*319|    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n*320|    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n 321|    // On Unix the cfg(windows) teardown strips out and the idempotence\n 322|    // guard's early `return` becomes the function tail — needless_return\n 323|    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n...\n 330|        #[cfg(windows)]\n*331|        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n 332|        unsafe {\n 333|            let _ = win::DisconnectNamedPipe(self.raw);\n 334|            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n...\n 338|    /// The watchdog body: park until a write is in flight, then sleep to its\n*339|    /// absolute deadline. If the op is still in flight at the deadline,\n*340|    /// poison + cancel, re-canceling periodically until the op reports\n 341|    /// completion (the writer clears `inflight` the instant `write_frame`\n 342|    /// returns — that return IS the buffer-release handshake), then sever.\n 343|    fn watchdog(&self) {\n...\n 346|            // Park until armed (or shutdown).\n*347|            let (op, deadline) = loop {\n 348|                if d.shutdown {\n 349|                    return;\n 350|                }\n...\n 358|            };\n*359|            // Sleep toward the deadline while THIS op stays in flight.\n 360|            let fired = loop {\n 361|                if d.shutdown {\n 362|                    return;\n...\n 365|                    Some((cur, _)) if cur == op => {}\n*366|                    // Completed (or a newer op armed) before the deadline.\n 367|                    _ => break false,\n 368|                }\n 369|                let now = Instant::now();\n*370|                if now >= deadline {\n 371|                    break true;\n 372|                }\n 373|                let (g, _) = self\n 374|                    .dog_cv\n*375|                    .wait_timeout(d, deadline - now)\n 376|                    .unwrap_or_else(|p| p.into_inner());\n 377|                d = g;\n 378|            };\n...\n 381|            }\n*382|            // Deadline: abort out of band, then wait for the canceled op to\n*383|            // report completion before severing (re-cancel each slice — a\n*384|            // cancel that raced the WriteFileEx issuance reaches nothing, so\n 385|            // one shot is not enough). Latch timed_out FIRST so the aborted\n*386|            // writer observes its failure as a DEADLINE retirement (wedge),\n 387|            // not an organic conn death.\n 388|            drop(d);\n 389|            self.timed_out.store(true, Ordering::Release);\n*390|            self.poison_and_cancel(None);\n 391|            d = recover(&self.dog);\n 392|            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n 393|                let (g, _) = self\n...\n 398|                drop(d);\n*399|                self.poison_and_cancel(None);\n 400|                d = recover(&self.dog);\n 401|            }\n 402|            if d.shutdown {\n...\n 414|/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\n*415|pub(crate) struct BrokerConn {\n 416|    inner: Arc<Inner>,\n 417|    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n 418|    /// watchdog aborts through strictly outlives every raw use.\n...\n 423|    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n*424|    /// deadline (gate-wait + OS write completion) — the broker passes its\n*425|    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n 426|    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n 427|        let raw = raw_of(&half);\n 428|        let inner = Arc::new(Inner {\n...\n 438|            dog_cv: Condvar::new(),\n*439|            poisoned: AtomicBool::new(false),\n 440|            timed_out: AtomicBool::new(false),\n 441|            severed: AtomicBool::new(false),\n 442|            graveyard: Mutex::new(None),\n...\n 490|\n*491|    /// Bounded framed write. See [`Self::write_gated`].\n*492|    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n*493|        self.write_gated(env, || true)\n 494|    }\n 495|\n 496|    /// Bounded framed write with a pre-write gate closure, run AFTER the\n...\n 501|    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n*502|    pub(crate) fn write_gated(\n 503|        &self,\n 504|        env: &Envelope,\n 505|        pre: impl FnOnce() -> bool,\n...\n 507|        let inner = &*self.inner;\n*508|        let deadline = Instant::now() + inner.bound;\n 509|\n*510|        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n 511|        let mut g = recover(&inner.gate);\n 512|        loop {\n*513|            if inner.poisoned.load(Ordering::Acquire) {\n*514|                return Err(poisoned_err());\n 515|            }\n 516|            if g.half.is_some() {\n 517|                break;\n...\n 519|            let now = Instant::now();\n*520|            if now >= deadline {\n 521|                // The write we queued behind is stuck past OUR whole bound:\n 522|                // that op is the wedge. Abort it out of band and fail — the\n*523|                // watchdog's own deadline (armed at that op's entry) is also\n 524|                // in flight; both paths converge on the same idempotent\n*525|                // poison/cancel.\n 526|                drop(g);\n 527|                inner.timed_out.store(true, Ordering::Release);\n*528|                inner.poison_and_cancel(None);\n 529|                return Err(timeout_err());\n 530|            }\n 531|            let (g2, _) = inner\n 532|                .gate_cv\n*533|                .wait_timeout(g, deadline - now)\n 534|                .unwrap_or_else(|p| p.into_inner());\n 535|            g = g2;\n 536|        }\n...\n 542|        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n*543|        // first committed write, so a conn that later retires can be correlated\n*544|        // to when it went active (fresh-carrier churn shows as open→start→retire\n 545|        // triplets with fresh ids).\n 546|        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n 547|            inner.lifecycle(\"write-start\", \"\");\n...\n 554|            let mut d = recover(&inner.dog);\n*555|            d.inflight = Some((op, deadline));\n 556|            inner.dog_cv.notify_all();\n 557|        }\n 558|        drop(g);\n...\n 562|        // Completion handshake: `write_frame` has returned, so the OS op —\n*563|        // canceled or not — reported completion and the frame buffer is\n 564|        // released. Clear the arm BEFORE deciding health, so the watchdog\n 565|        // never severs while the op is formally in flight.\n 566|        {\n...\n 572|        let mut g = recover(&inner.gate);\n*573|        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n 574|            g.half = Some(half);\n 575|            drop(g);\n 576|            inner.gate_cv.notify_all();\n...\n 579|\n*580|        // ── Retire: poison the whole conn, abort the read side so\n 581|        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n 582|        // graveyard (its handle must outlive any late raw abort; never\n 583|        // re-gated, never reused — a timed-out length-prefixed frame may be\n...\n 585|        drop(g);\n*586|        inner.poison_and_cancel(res.as_ref().err());\n 587|        inner.sever();\n 588|        *recover(&inner.graveyard) = Some(half);\n 589|        inner.gate_cv.notify_all();\n*590|        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n*591|        // already deadline-poisoned) surfaces as TimedOut — the caller can\n 592|        // count the wedge (W2 stall-evict tally). An organic I/O failure\n 593|        // (peer death) keeps its own error.\n 594|        if inner.timed_out.load(Ordering::Acquire) {\n...\n 598|            Err(e) => e,\n*599|            Ok(()) => poisoned_err(),\n 600|        })\n 601|    }\n 602|}\n...\n 626|    /// Check the half out of the gate without arming the watchdog — writers\n*627|    /// queue behind the gate (bounded by their own deadlines) until the\n 628|    /// returned pin drops. Test-only; never a production path.\n 629|    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n 630|        let mut g = recover(&self.inner.gate);\n...\n 642|        // is about to close — the terminal record of this conn id's life, with\n*643|        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n 644|        self.inner.lifecycle(\n 645|            \"transport-close\",\n 646|            &format!(\n*647|                \"poisoned={} timed_out={}\",\n*648|                self.inner.poisoned.load(Ordering::Acquire),\n 649|                self.inner.timed_out.load(Ordering::Acquire)\n 650|            ),\n 651|        );\n...\n 664|\n*665|fn poisoned_err() -> io::Error {\n 666|    io::Error::new(\n 667|        io::ErrorKind::BrokenPipe,\n*668|        \"broker conn poisoned (bounded write retired the physical connection)\",\n 669|    )\n 670|}\n 671|\n...\n 674|        io::ErrorKind::TimedOut,\n*675|        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n 676|    )\n 677|}\n 678|\n...\n 707|    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*708|    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n 709|    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 710|    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 711|    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n...\n 714|    #[test]\n*715|    fn retirement_token_splits_deadline_from_organic_with_attribution_parity() {\n 716|        let (conn, _client) = make_conn();\n 717|        conn.describe(\"role=brain\");\n 718|        conn.describe(\"controller session=7 endpoint=ep by=cli@host\");\n 719|\n*720|        let poisoned = conn.inner.render_retirement(true, None);\n 721|        assert!(\n*722|            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n*723|            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 724|        );\n*725|        let retired = conn\n 726|            .inner\n*727|            .render_retirement(false, Some(&io::Error::new(io::ErrorKind::BrokenPipe, \"peer gone\")));\n 728|        assert!(\n*729|            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n*730|            \"organic class emits the DISTINCT retired token: {retired}\"\n 731|        );\n 732|        assert!(\n*733|            retired.contains(\"BrokenPipe\") && retired.contains(\"peer gone\"),\n*734|            \"the organic record carries the error kind + message: {retired}\"\n 735|        );\n 736|        // Attribution parity: every field present on BOTH tokens.\n*737|        for (name, line) in [(\"POISONED\", &poisoned), (\"RETIRED\", &retired)] {\n 738|            assert!(\n 739|                line.contains(&format!(\"conn={}\", conn.id())),\n 740|                \"{name} carries the stable conn id: {line}\"\n...\n 754|    // split (the REQ-HAZARD-SHAREDSEND invariant): an organic fast-fail still\n*755|    // poisons + retires the conn — it is only the CLASS that differs (the error\n 756|    // surfaces with its own kind, never TimedOut, and the next write refuses).\n 757|    #[test]\n*758|    fn organic_write_failure_still_poisons_and_is_not_the_timeout_class() {\n 759|        let (conn, client) = make_conn();\n*760|        drop(client); // peer dies → writes fail organically (no deadline involved)\n 761|        let env = Envelope::new(\"attr-test\", serde_json::json!({\"n\": 1}));\n 762|        let mut organic = None;\n 763|        // A first write can land in the OS buffer before the peer-death is\n...\n 765|        for _ in 0..200 {\n*766|            match conn.write(&env) {\n 767|                Err(e) => {\n 768|                    organic = Some(e);\n 769|                    break;\n...\n 777|            io::ErrorKind::TimedOut,\n*778|            \"an organic failure must NOT surface as the deadline class\"\n 779|        );\n*780|        // Poisoned + retired: the next write refuses immediately.\n*781|        let refused = conn.write(&env).expect_err(\"poisoned conn refuses writes\");\n 782|        assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);\n 783|    }\n 784|\n...\n 808|        // The id (and the capped label) stay stable across records.\n*809|        let one = a.inner.render_retirement(true, None);\n*810|        let two = a.inner.render_retirement(true, None);\n 811|        assert!(one.contains(&format!(\"conn={}\", a.id())));\n 812|        assert!(two.contains(&format!(\"conn={}\", a.id())));\n 813|    }"}],"details":{"scopePath":"crates/spt-daemon/src/conn.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":117,"fileCount":1,"files":["crates/spt-daemon/src/conn.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/conn.rs","count":117}],"truncated":false,"displayContent":"  *1│//! Bounded, cancelable, poison-on-failure framed writes for one physical\n   2│//! broker connection (UPDATE-WEDGE round-4).\n   3│//!\n   4│//! ## The failure class this kills\n    │...\n  12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n *13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n  14│//! connection, so the writer (and every write queued behind the same gate)\n  15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n  16│//! brain restart).\n    │...\n  23│//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n *24│//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n  25│//!    write);\n  26│//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n *27│//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n  28│//! 3. on success, returns the half to the gate.\n  29│//!\n *30│//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n *31│//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n  32│//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n  33│//! completion APC makes the wedged `SleepEx` loop return with\n  34│//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n *35│//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n  36│//! the same abort (the stuck op it waited behind is the wedge).\n  37│//!\n *38│//! ## Poison / retire invariant (both OSes)\n  39│//!\n *40│//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n *41│//! connection is poisoned, idempotently — a length-prefixed frame may have\n  42│//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n  43│//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n *44│//! (3) the buffer of the canceled op is released only after the op reports\n  45│//! completion (`write_frame` returns only after the completion APC — the\n *46│//! synchronous return IS the completion handshake); (4) the retired half is\n  47│//! parked in a graveyard slot — never dropped while the watchdog might still\n  48│//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n  49│//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n    │...\n  53│//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n *54│//! cancel→disconnect window.\n  55│//!\n  56│//! No new output queue (the bounded per-sink queues + isolated writer threads\n  57│//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n  58│//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n *59│//! healthy connections; only the poison/retire path is new.\n  60│// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n  61│\n  62│use std::io;\n    │...\n 114│\n*115│/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n 116│#[derive(Debug, PartialEq, Eq)]\n 117│pub(crate) enum ConnWrite {\n 118│    /// The frame was fully written.\n    │...\n 125│/// The serialized write gate. `half: Some` = free; `None` = a write is in\n*126│/// flight (checked out) or the conn has retired (then `poisoned` is set).\n 127│struct Gate {\n 128│    half: Option<SendHalf>,\n 129│}\n 130│\n*131│/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\n 132│struct Dog {\n*133│    /// `Some((op_id, deadline))` while a write is in flight.\n 134│    inflight: Option<(u64, Instant)>,\n 135│    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136│    shutdown: bool,\n    │...\n 152│    dog_cv: Condvar,\n*153│    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n 154│    /// conn refuses every future write and is never re-gated.\n*155│    poisoned: AtomicBool,\n*156│    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n 157│    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n 158│    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n 159│    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    │...\n 163│    severed: AtomicBool,\n*164│    /// A retired (poisoned) half parks here so its OS handle stays alive\n 165│    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n 166│    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n 167│    graveyard: Mutex<Option<SendHalf>>,\n*168│    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n 169│    op_seq: AtomicU64,\n 170│    /// The absolute per-write bound (gate-wait + OS write completion).\n 171│    bound: Duration,\n    │...\n 228│\n*229│    /// Render the once-per-conn retirement record — the F-039 token split\n*230│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233│    /// already-dead counterpart, the offline-peer dial churn) emits the\n 234│    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n*235│    /// SAME attribution fields — quieter class, never thinner (the retired\n 236│    /// class is what discriminates fresh-carrier churn from interleave\n*237│    /// artifact in the field). Behavior is identical either way: poison,\n*238│    /// abort, retire, never reuse.\n 239│    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n*240│    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n 241│        if timed_out {\n 242│            format!(\n*243│                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 244│                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 245│                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 246│                self.attribution(),\n    │...\n 255│                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n*256│                 broker conn poisoned + retired, never reused (same retire behavior \\\n 257│                 as POISONED — only the failure class differs) \\\n 258│                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n 259│                self.attribution()\n    │...\n 267│    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n*268│    /// timeout/cancel leg is the retirement record itself.\n 269│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 270│    fn lifecycle(&self, event: &str, extra: &str) {\n 271│        let sep = if extra.is_empty() { \"\" } else { \" \" };\n    │...\n 278│    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n*279│    /// the halves share one OS object) and latch the poison. Idempotent and\n 280│    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n*281│    /// no-op, and re-cancel closes the race where a write was issued a beat\n*282│    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n*283│    /// `cause` (the organic caller's write error) rides the retirement record\n*284│    /// when the retirement is NOT deadline-driven.\n*285│    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n*286│        if !self.poisoned.swap(true, Ordering::AcqRel) {\n*287│            // First poison of this conn: loud, once. The token is class-split\n*288│            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n 289│            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 290│            eprintln!(\n 291│                \"{}\",\n*292│                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n 293│            );\n 294│        }\n 295│        #[cfg(windows)]\n    │...\n 307│        }\n*308│        // Wake gate-waiters so they observe the poison instead of sleeping\n*309│        // out their full deadline, and the watchdog so it re-evaluates.\n 310│        self.gate_cv.notify_all();\n 311│        self.dog_cv.notify_all();\n 312│    }\n 313│\n*314│    /// Sever the physical connection (exactly once), AFTER the canceled op\n 315│    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n 316│    /// client off (its next access fails), makes a re-issued server-side\n 317│    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n*318│    /// new read in after the cancel), and defuses the drop-limbo flush; the\n*319│    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n*320│    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n 321│    // On Unix the cfg(windows) teardown strips out and the idempotence\n 322│    // guard's early `return` becomes the function tail — needless_return\n 323│    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    │...\n 330│        #[cfg(windows)]\n*331│        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n 332│        unsafe {\n 333│            let _ = win::DisconnectNamedPipe(self.raw);\n 334│            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n    │...\n 338│    /// The watchdog body: park until a write is in flight, then sleep to its\n*339│    /// absolute deadline. If the op is still in flight at the deadline,\n*340│    /// poison + cancel, re-canceling periodically until the op reports\n 341│    /// completion (the writer clears `inflight` the instant `write_frame`\n 342│    /// returns — that return IS the buffer-release handshake), then sever.\n 343│    fn watchdog(&self) {\n    │...\n 346│            // Park until armed (or shutdown).\n*347│            let (op, deadline) = loop {\n 348│                if d.shutdown {\n 349│                    return;\n 350│                }\n    │...\n 358│            };\n*359│            // Sleep toward the deadline while THIS op stays in flight.\n 360│            let fired = loop {\n 361│                if d.shutdown {\n 362│                    return;\n    │...\n 365│                    Some((cur, _)) if cur == op => {}\n*366│                    // Completed (or a newer op armed) before the deadline.\n 367│                    _ => break false,\n 368│                }\n 369│                let now = Instant::now();\n*370│                if now >= deadline {\n 371│                    break true;\n 372│                }\n 373│                let (g, _) = self\n 374│                    .dog_cv\n*375│                    .wait_timeout(d, deadline - now)\n 376│                    .unwrap_or_else(|p| p.into_inner());\n 377│                d = g;\n 378│            };\n    │...\n 381│            }\n*382│            // Deadline: abort out of band, then wait for the canceled op to\n*383│            // report completion before severing (re-cancel each slice — a\n*384│            // cancel that raced the WriteFileEx issuance reaches nothing, so\n 385│            // one shot is not enough). Latch timed_out FIRST so the aborted\n*386│            // writer observes its failure as a DEADLINE retirement (wedge),\n 387│            // not an organic conn death.\n 388│            drop(d);\n 389│            self.timed_out.store(true, Ordering::Release);\n*390│            self.poison_and_cancel(None);\n 391│            d = recover(&self.dog);\n 392│            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n 393│                let (g, _) = self\n    │...\n 398│                drop(d);\n*399│                self.poison_and_cancel(None);\n 400│                d = recover(&self.dog);\n 401│            }\n 402│            if d.shutdown {\n    │...\n 414│/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\n*415│pub(crate) struct BrokerConn {\n 416│    inner: Arc<Inner>,\n 417│    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n 418│    /// watchdog aborts through strictly outlives every raw use.\n    │...\n 423│    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n*424│    /// deadline (gate-wait + OS write completion) — the broker passes its\n*425│    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n 426│    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n 427│        let raw = raw_of(&half);\n 428│        let inner = Arc::new(Inner {\n    │...\n 438│            dog_cv: Condvar::new(),\n*439│            poisoned: AtomicBool::new(false),\n 440│            timed_out: AtomicBool::new(false),\n 441│            severed: AtomicBool::new(false),\n 442│            graveyard: Mutex::new(None),\n    │...\n 490│\n*491│    /// Bounded framed write. See [`Self::write_gated`].\n*492│    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n*493│        self.write_gated(env, || true)\n 494│    }\n 495│\n 496│    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    │...\n 501│    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n*502│    pub(crate) fn write_gated(\n 503│        &self,\n 504│        env: &Envelope,\n 505│        pre: impl FnOnce() -> bool,\n    │...\n 507│        let inner = &*self.inner;\n*508│        let deadline = Instant::now() + inner.bound;\n 509│\n*510│        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n 511│        let mut g = recover(&inner.gate);\n 512│        loop {\n*513│            if inner.poisoned.load(Ordering::Acquire) {\n*514│                return Err(poisoned_err());\n 515│            }\n 516│            if g.half.is_some() {\n 517│                break;\n    │...\n 519│            let now = Instant::now();\n*520│            if now >= deadline {\n 521│                // The write we queued behind is stuck past OUR whole bound:\n 522│                // that op is the wedge. Abort it out of band and fail — the\n*523│                // watchdog's own deadline (armed at that op's entry) is also\n 524│                // in flight; both paths converge on the same idempotent\n*525│                // poison/cancel.\n 526│                drop(g);\n 527│                inner.timed_out.store(true, Ordering::Release);\n*528│                inner.poison_and_cancel(None);\n 529│                return Err(timeout_err());\n 530│            }\n 531│            let (g2, _) = inner\n 532│                .gate_cv\n*533│                .wait_timeout(g, deadline - now)\n 534│                .unwrap_or_else(|p| p.into_inner());\n 535│            g = g2;\n 536│        }\n    │...\n 542│        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n*543│        // first committed write, so a conn that later retires can be correlated\n*544│        // to when it went active (fresh-carrier churn shows as open→start→retire\n 545│        // triplets with fresh ids).\n 546│        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n 547│            inner.lifecycle(\"write-start\", \"\");\n    │...\n 554│            let mut d = recover(&inner.dog);\n*555│            d.inflight = Some((op, deadline));\n 556│            inner.dog_cv.notify_all();\n 557│        }\n 558│        drop(g);\n    │...\n 562│        // Completion handshake: `write_frame` has returned, so the OS op —\n*563│        // canceled or not — reported completion and the frame buffer is\n 564│        // released. Clear the arm BEFORE deciding health, so the watchdog\n 565│        // never severs while the op is formally in flight.\n 566│        {\n    │...\n 572│        let mut g = recover(&inner.gate);\n*573│        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n 574│            g.half = Some(half);\n 575│            drop(g);\n 576│            inner.gate_cv.notify_all();\n    │...\n 579│\n*580│        // ── Retire: poison the whole conn, abort the read side so\n 581│        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n 582│        // graveyard (its handle must outlive any late raw abort; never\n 583│        // re-gated, never reused — a timed-out length-prefixed frame may be\n    │...\n 585│        drop(g);\n*586│        inner.poison_and_cancel(res.as_ref().err());\n 587│        inner.sever();\n 588│        *recover(&inner.graveyard) = Some(half);\n 589│        inner.gate_cv.notify_all();\n*590│        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n*591│        // already deadline-poisoned) surfaces as TimedOut — the caller can\n 592│        // count the wedge (W2 stall-evict tally). An organic I/O failure\n 593│        // (peer death) keeps its own error.\n 594│        if inner.timed_out.load(Ordering::Acquire) {\n    │...\n 598│            Err(e) => e,\n*599│            Ok(()) => poisoned_err(),\n 600│        })\n 601│    }\n 602│}\n    │...\n 626│    /// Check the half out of the gate without arming the watchdog — writers\n*627│    /// queue behind the gate (bounded by their own deadlines) until the\n 628│    /// returned pin drops. Test-only; never a production path.\n 629│    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n 630│        let mut g = recover(&self.inner.gate);\n    │...\n 642│        // is about to close — the terminal record of this conn id's life, with\n*643│        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n 644│        self.inner.lifecycle(\n 645│            \"transport-close\",\n 646│            &format!(\n*647│                \"poisoned={} timed_out={}\",\n*648│                self.inner.poisoned.load(Ordering::Acquire),\n 649│                self.inner.timed_out.load(Ordering::Acquire)\n 650│            ),\n 651│        );\n    │...\n 664│\n*665│fn poisoned_err() -> io::Error {\n 666│    io::Error::new(\n 667│        io::ErrorKind::BrokenPipe,\n*668│        \"broker conn poisoned (bounded write retired the physical connection)\",\n 669│    )\n 670│}\n 671│\n    │...\n 674│        io::ErrorKind::TimedOut,\n*675│        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n 676│    )\n 677│}\n 678│\n    │...\n 707│    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*708│    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n 709│    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 710│    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 711│    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n    │...\n 714│    #[test]\n*715│    fn retirement_token_splits_deadline_from_organic_with_attribution_parity() {\n 716│        let (conn, _client) = make_conn();\n 717│        conn.describe(\"role=brain\");\n 718│        conn.describe(\"controller session=7 endpoint=ep by=cli@host\");\n 719│\n*720│        let poisoned = conn.inner.render_retirement(true, None);\n 721│        assert!(\n*722│            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n*723│            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 724│        );\n*725│        let retired = conn\n 726│            .inner\n*727│            .render_retirement(false, Some(&io::Error::new(io::ErrorKind::BrokenPipe, \"peer gone\")));\n 728│        assert!(\n*729│            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n*730│            \"organic class emits the DISTINCT retired token: {retired}\"\n 731│        );\n 732│        assert!(\n*733│            retired.contains(\"BrokenPipe\") && retired.contains(\"peer gone\"),\n*734│            \"the organic record carries the error kind + message: {retired}\"\n 735│        );\n 736│        // Attribution parity: every field present on BOTH tokens.\n*737│        for (name, line) in [(\"POISONED\", &poisoned), (\"RETIRED\", &retired)] {\n 738│            assert!(\n 739│                line.contains(&format!(\"conn={}\", conn.id())),\n 740│                \"{name} carries the stable conn id: {line}\"\n    │...\n 754│    // split (the REQ-HAZARD-SHAREDSEND invariant): an organic fast-fail still\n*755│    // poisons + retires the conn — it is only the CLASS that differs (the error\n 756│    // surfaces with its own kind, never TimedOut, and the next write refuses).\n 757│    #[test]\n*758│    fn organic_write_failure_still_poisons_and_is_not_the_timeout_class() {\n 759│        let (conn, client) = make_conn();\n*760│        drop(client); // peer dies → writes fail organically (no deadline involved)\n 761│        let env = Envelope::new(\"attr-test\", serde_json::json!({\"n\": 1}));\n 762│        let mut organic = None;\n 763│        // A first write can land in the OS buffer before the peer-death is\n    │...\n 765│        for _ in 0..200 {\n*766│            match conn.write(&env) {\n 767│                Err(e) => {\n 768│                    organic = Some(e);\n 769│                    break;\n    │...\n 777│            io::ErrorKind::TimedOut,\n*778│            \"an organic failure must NOT surface as the deadline class\"\n 779│        );\n*780│        // Poisoned + retired: the next write refuses immediately.\n*781│        let refused = conn.write(&env).expect_err(\"poisoned conn refuses writes\");\n 782│        assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);\n 783│    }\n 784│\n    │...\n 808│        // The id (and the capped label) stay stable across records.\n*809│        let one = a.inner.render_retirement(true, None);\n*810│        let two = a.inner.render_retirement(true, None);\n 811│        assert!(one.contains(&format!(\"conn={}\", a.id())));\n 812│        assert!(two.contains(&format!(\"conn={}\", a.id())));\n 813│    }"},"isError":false,"timestamp":1784195796273}}
{"type":"message","id":"d73a3118","parentId":"74a35f03","timestamp":"2026-07-16T09:56:36.289Z","message":{"role":"toolResult","toolCallId":"call_eHn52OXWtLcpK1oTsIZFEtd0|fc_0cc7a0c9a797f232016a58aad43540819abefc85398241e059","toolName":"read","content":[{"type":"text","text":"///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\n/// `net-status` reply rather than the broker's identity load.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n// [impl->REQ-DAEMON-9]\nfn spawn_net_consumers(node_hex: &str) {\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n    let stop = Arc::new(AtomicBool::new(false));\n    let _ = crate::dispatch::spawn_dispatcher(\n        broker_socket_name(),\n        Arc::clone(&registry),\n        Arc::clone(&stop),\n    );\n    let cfg = crate::config::DaemonConfig::load();\n    if cfg.peer_pump {\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n    }\n}\n\n/// Connect to the broker, retrying briefly while it finishes binding.\n…\nfn brain_child_args(generation: u64, reason: StartReason) -> Vec<String> {\n    vec![\n        \"daemon\".to_string(),\n        \"brain\".to_string(),\n        \"--generation\".to_string(),\n        generation.to_string(),\n        \"--start-reason\".to_string(),\n        reason.as_arg().to_string(),\n    ]\n}\n\n/// Production wiring: run the brain supervisor on its own thread, spawned by the\n/// broker process ([`crate::daemon::Daemon::run`]) before its foreground\n/// seed-control loop. The brain child is supervised for the broker's lifetime.\n/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn spawn_brain_supervisor(\n    stop: Arc<AtomicBool>,\n    reaper: Arc<crate::reap::BrainReaper>,\n    broker: Option<Arc<crate::broker::Broker>>,\n) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n    let restart = Arc::new(BrainRestart::new());\n    let restart_sup = Arc::clone(&restart);\n    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n    // start, before any `apply` can rename it under us. Linux current_exe()\n    // (readlink /proc/self/exe) is inode-tracking and would later follow the\n    // apply-rename (spt → spt.old-N) to the OLD bytes; capturing at t=0 pins the\n    // canonical path string, matching Windows' path-at-start semantics. The one\n    // true race — an apply landing in the microseconds between broker exec and\n    // this line — is vanishingly rare and backstopped by the promotion\n    // bytes-gate (a wrong-bytes trial rolls back loudly, never promotes).\n    let canonical_exe = std::env::current_exe().ok();\n    let handle = thread::spawn(move || {\n        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n        // readiness-only (no old-gen control plane to drain).\n        let env = ProductionTrialEnv { broker };\n        supervise_brain(\n            &stop,\n            &restart_sup,\n            SUPERVISE_BACKOFF_BASE,\n            &env,\n            SUPERVISE_HEALTHY_RUN,\n            |generation, reason, binary| {\n                let child =\n                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;\n                // Enroll the (re)spawned brain into the reap container so a\n                // `daemon stop` tears down the brain + its Psyches as one unit\n                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n                reaper.enroll(&child);\n                Ok(child)\n            },\n        );\n    });\n    (handle, restart)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::sync::atomic::AtomicU32;\n\n    /// A trivial, fast-exiting real child — the cross-OS no-op the supervisor\n    /// test respawns (stands in for the brain process).\n    fn fast_child() -> io::Result<Child> {\n        #[cfg(windows)]\n        {\n…\n        }\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\n/// `net-status` reply rather than the broker's identity load.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n// [impl->REQ-DAEMON-9]\nfn spawn_net_consumers(node_hex: &str) {\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n    let stop = Arc::new(AtomicBool::new(false));\n    let _ = crate::dispatch::spawn_dispatcher(\n        broker_socket_name(),\n        Arc::clone(&registry),\n        Arc::clone(&stop),\n    );\n    let cfg = crate::config::DaemonConfig::load();\n    if cfg.peer_pump {\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n    }\n}\n\n/// Connect to the broker, retrying briefly while it finishes binding.\n…\nfn brain_child_args(generation: u64, reason: StartReason) -> Vec<String> {\n    vec![\n        \"daemon\".to_string(),\n        \"brain\".to_string(),\n        \"--generation\".to_string(),\n        generation.to_string(),\n        \"--start-reason\".to_string(),\n        reason.as_arg().to_string(),\n    ]\n}\n\n/// Production wiring: run the brain supervisor on its own thread, spawned by the\n/// broker process ([`crate::daemon::Daemon::run`]) before its foreground\n/// seed-control loop. The brain child is supervised for the broker's lifetime.\n/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn spawn_brain_supervisor(\n    stop: Arc<AtomicBool>,\n    reaper: Arc<crate::reap::BrainReaper>,\n    broker: Option<Arc<crate::broker::Broker>>,\n) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n    let restart = Arc::new(BrainRestart::new());\n    let restart_sup = Arc::clone(&restart);\n    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n    // start, before any `apply` can rename it under us. Linux current_exe()\n    // (readlink /proc/self/exe) is inode-tracking and would later follow the\n    // apply-rename (spt → spt.old-N) to the OLD bytes; capturing at t=0 pins the\n    // canonical path string, matching Windows' path-at-start semantics. The one\n    // true race — an apply landing in the microseconds between broker exec and\n    // this line — is vanishingly rare and backstopped by the promotion\n    // bytes-gate (a wrong-bytes trial rolls back loudly, never promotes).\n    let canonical_exe = std::env::current_exe().ok();\n    let handle = thread::spawn(move || {\n        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n        // readiness-only (no old-gen control plane to drain).\n        let env = ProductionTrialEnv { broker };\n        supervise_brain(\n            &stop,\n            &restart_sup,\n            SUPERVISE_BACKOFF_BASE,\n            &env,\n            SUPERVISE_HEALTHY_RUN,\n            |generation, reason, binary| {\n                let child =\n                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;\n                // Enroll the (re)spawned brain into the reap container so a\n                // `daemon stop` tears down the brain + its Psyches as one unit\n                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n                reaper.enroll(&child);\n                Ok(child)\n            },\n        );\n    });\n    (handle, restart)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::sync::atomic::AtomicU32;\n\n    /// A trivial, fast-exiting real child — the cross-OS no-op the supervisor\n    /// test respawns (stands in for the brain process).\n    fn fast_child() -> io::Result<Child> {\n        #[cfg(windows)]\n        {\n…\n        }\n…\n    }\n…\n}","startLine":155,"lineNumbers":[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,null,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,null,1147,null,1152,null,2115]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"}}},"isError":false,"timestamp":1784195796289}}
{"type":"message","id":"205922ef","parentId":"d73a3118","timestamp":"2026-07-16T09:56:36.292Z","message":{"role":"toolResult","toolCallId":"call_f0GzbkXGxaHIVQoe0fsCdXcj|fc_0cc7a0c9a797f232016a58aad43530819aa39affcac0a8b54f","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### brain.rs\n 76|/// respawns reconstructs continuity by querying the broker\n*77|/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n 78|/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n 79|/// solely because the integration tests (separate test target — `#[cfg(test)]`\n 80|/// would hide them) still drive the handoff-frame shape directly; there is **no\n...\n 282|    /// continuity state a cold-started brain rebuilds from the broker via\n*283|    /// [`Brain::resume_sessions`], replacing the retired single-session\n 284|    /// `BrainState` handoff. A session present here is in **resume mode** — its\n 285|    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n 286|    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n...\n 380|    /// **Test-only (restoration D4-3).** The production resume path is\n*381|    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n 382|    /// never this brain→brain frame; `handoff` survives only for the integration\n 383|    /// tests. See [`BrainState`].\n 384|    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n...\n 389|        // `read_event` routes this session through the dedup-below + snap-above\n*390|        // path (matching `resume_sessions`), not the legacy reject-gap path that\n 391|        // turns any at-least-once boundary reorder into a fatal `output gap`.\n 392|        let mut session_cursors = std::collections::HashMap::new();\n 393|        session_cursors.insert(prior.session_id, prior.next_seq);\n...\n 642|                    // A **resume-mode** brain (the per-session map is populated by\n*643|                    // `resume_sessions`) routes EVERY session through the map —\n 644|                    // including a freshly-spawned or first-seen one, which seeds\n 645|                    // itself at its first `seq` via `entry().or_insert` (D4-2b,\n 646|                    // doyle amendment 5: this closes the mixed-mode gap where N≥2\n...\n 1036|    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n*1037|    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n 1038|    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n*1039|    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n 1040|    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n 1041|    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n 1042|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n...\n 1045|    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*1046|    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n*1047|        let reply = self.sessions()?;\n 1048|        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1049|        for info in reply.sessions {\n 1050|            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n...\n 1063|    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n*1064|    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n 1065|    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n 1066|    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n 1067|    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n*1068|    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n 1069|    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n 1070|    /// the observation cursor is all it needs; no attach also means no controller steal\n 1071|    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n...\n 1076|    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*1077|    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n*1078|        let reply = self.sessions()?;\n 1079|        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1080|        for info in reply.sessions {\n 1081|            self.session_cursors\n...\n 1088|    /// The per-session resume cursor (the next seq still needed for a session\n*1089|    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.\n 1090|    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {\n 1091|        self.session_cursors.get(&session_id).copied()\n 1092|    }\n...\n 1605|    /// `spt rc`) it never drains, freezing the session\n*1606|    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL). [`Brain::resume_sessions`] re-attaches\n 1607|    /// as Viewer instead; Control is reserved for sessions the daemon brain drives.\n 1608|    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 1609|    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n### brainproc.rs\n 193|    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n*194|    // this conn (the heartbeat below reads it only in net_status()/sessions()\n 195|    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n 196|    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n*197|    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n 198|    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n*199|    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n 200|    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n 201|    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*202|    match brain.resume_session_cursors() {\n 203|        Ok(ids) if !ids.is_empty() => {\n 204|            eprintln!(\n 205|                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n...\n 220|    // semantic the live-agent adapter inherits.\n*221|    write_ready(generation);\n 222|\n 223|    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n 224|    // online/offline exclusivity — a reconcile loop that runs each offline\n...\n 250|    // reconcile in the background (ready-without-warm is preserved because\n*251|    // write_ready already fired above), then drain/debounce/coalesce the\n 252|    // invalidation queue. Same posture as the hosts above: the stop flag is\n 253|    // held for symmetry, the thread dies with the brain, and the next brain\n 254|    // rehydrates from the persisted index (warm start, fingerprint-gated).\n...\n 263|        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n*264|        let status = match brain.net_status() {\n 265|            Ok(s) => s,\n 266|            Err(e) => {\n 267|                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n...\n 285|        // window, so without this the wedge never drains: the promotion DRAINED gate\n*286|        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n 287|        // drives the evict) stays false for the full trial → \"alive but never ready\"\n 288|        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n 289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n...\n 295|        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n*296|        let _ = brain.sessions();\n*297|        write_ready(generation);\n 298|    }\n 299|}\n 300|\n...\n 364|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*365|fn write_ready(generation: u64) {\n 366|    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n 367|    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n 368|    // tolerates its absence). Never fail the ready write over a diagnostic field.\n...\n 481|    /// strands an apply where no old-gen control plane exists.\n*482|    fn old_gen_drained(&self) -> bool;\n 483|}\n 484|\n 485|/// The no-trial environment: every probe degrades to \"nothing pending / not\n...\n 505|    }\n*506|    fn old_gen_drained(&self) -> bool {\n 507|        // No readiness gate to drive here → no old-gen control plane to drain; the\n 508|        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n 509|        // no-op effects).\n...\n 519|/// Holds the live [`crate::broker::Broker`] handle (or `None` on a net-less /\n*520|/// bind-failed daemon) so the DRAINED gate ([`TrialEnv::old_gen_drained`]) reads\n 521|/// broker truth IN-PROCESS — the supervisor thread runs inside the broker process,\n 522|/// so this is a direct memory read, never an IPC round-trip through a possibly-\n 523|/// wedged conn (REQ-UPDATE-PROMOTE-DRAINED).\n...\n 571|    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n*572|    fn old_gen_drained(&self) -> bool {\n 573|        // Drained ⟺ no hosted session has a wedged LOCAL (brain-own) controller conn.\n 574|        // No broker (net-less / bind-failed) → no control plane → drained. A broker\n 575|        // with no wedged local controller (the common case, incl. no daemon-hosted\n...\n 578|            .as_ref()\n*579|            .is_none_or(|b| !b.any_local_controller_wedged())\n 580|    }\n 581|}\n 582|\n...\n 671|/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n*672|/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n 673|/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n 674|/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n 675|/// the candidate and rolls back (conservative: never a false-promote onto a wedge).\n...\n 697|        }\n*698|        if ready_seen && env.old_gen_drained() {\n 699|            return TrialStep::Promoted;\n 700|        }\n 701|        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n...\n 1457|        staged_hash: Mutex<Option<String>>,\n*1458|        // DRAINED-gate countdown (REQ-UPDATE-PROMOTE-DRAINED): each `old_gen_drained`\n 1459|        // call returns `false` and decrements while > 0, then `true`. 0 (the default)\n 1460|        // = always drained, so every pre-existing trial test promotes exactly as\n 1461|        // before; `u32::MAX` = never drains within a test window; a small N = drains\n...\n 1492|\n*1493|        /// Arm the DRAINED gate: `old_gen_drained` returns `false` for the first\n 1494|        /// `polls` calls, then `true` (0 = always drained; `u32::MAX` = never within\n 1495|        /// the test window). Models the OLD-gen control plane draining after N polls.\n 1496|        fn drains_after(self, polls: u32) -> Self {\n\n## tests/\n### brain_resume_conn_deadlock.rs\n 4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n*5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n 6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n 8|//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n...\n 10|//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n*11|//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n 12|//! calls. When an actively-streaming session backs the conn up, the brain's own\n 13|//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n 14|//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n...\n 19|//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n*20|//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n*21|//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n 22|//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n 23|//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n 24|//! a truly deadlocked one completes ~zero).\n...\n 133|/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134|/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n*135|/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n 136|/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n 137|#[derive(Clone, Copy)]\n 138|enum ResumeMode {\n...\n 142|/// Drive `run_brain`'s heartbeat pattern on a fresh `Brain` conn: resume per `mode`,\n*143|/// then loop `net_status()` + `sessions()` — the exact per-tick IPC — incrementing a\n 144|/// shared counter per COMPLETED tick, so the caller can measure liveness in a window.\n 145|/// The probe NEVER drains subscribed output (it only issues request/reply, exactly like\n 146|/// the real non-draining daemon heartbeat) — so `CursorOnly` faithfully models the\n...\n 162|            ResumeMode::CursorOnly => {\n*163|                let _ = brain.resume_session_cursors();\n 164|            }\n 165|        }\n 166|        while !stop.load(Ordering::Relaxed) {\n 167|            // One heartbeat tick = the two IPC round-trips run_brain issues.\n*168|            if brain.net_status().is_err() {\n 169|                return;\n 170|            }\n*171|            if brain.sessions().is_err() {\n 172|                return;\n 173|            }\n 174|            ticks.fetch_add(1, Ordering::Relaxed);\n...\n 216|/// STEADY-STATE severity — the SHIPPING path: the daemon brain resumes CURSOR-ONLY\n*217|/// (`resume_session_cursors`, no subscribe) while N sessions actively stream, so its\n 218|/// heartbeat stays live. NO dead peer anywhere. RED-first: revert run_brain to the\n*219|/// subscribing `resume_sessions` (or see the regression-guard test below) and this\n 220|/// starves to a standstill.\n 221|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 222|#[test]\n...\n 265|/// Two settled arms, no timing window:\n*266|///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n 267|///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268|///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269|///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n...\n 271|///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\n*272|///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n 273|///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\n 274|///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\n 275|///   against an always-0 seam).\n...\n 278|/// (it writes the `KIND_SUBSCRIBE` frame and does NOT await `KIND_SUBSCRIBED`), and\n*279|/// `resume_sessions` runs its internal `sessions()` round-trip BEFORE sending those\n 280|/// subscribes — so on return the subscribes may not yet be applied. We force a\n*281|/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\n 282|/// the broker dispatches that conn's frames strictly in order and inserts each viewer\n 283|/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\n 284|/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\n...\n 287|/// retire/evict a viewer before we read the counts. State is settled the moment the\n*288|/// barrier `sessions()` returns.\n 289|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 290|#[test]\n 291|fn daemon_resume_leaves_zero_brain_subscribers() {\n...\n 315|    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\n*316|    brain.resume_session_cursors().expect(\"cursor-only resume\");\n 317|    for &sid in &sids {\n 318|        assert_eq!(\n 319|            broker.test_session_viewer_count(sid),\n...\n 329|    // SEAM-SENSITIVITY arm — a SECOND brain on the SAME broker takes the hazard path\n*330|    // (`resume_sessions` still exists) and MUST raise every count to >= 1, proving the\n 331|    // seam actually observes the subscription the positive arm forbids (else the Some(0)\n 332|    // above is a vacuous pass against an always-0 read).\n 333|    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\n*334|    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\n### false_promote.rs\n 13|//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n*14|//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n 15|//! read from BROKER TRUTH in-process (no brain round-trip).\n 16|//!\n 17|//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n...\n 40|//! 1. the trial does NOT promote while wedged (RED-first: remove the\n*41|//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n 42|//!    reproducing the 22:47 false-promote);\n 43|//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n 44|//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n...\n 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 229|    // The gate under test: REAL broker truth, no brain round-trip.\n*230|    fn old_gen_drained(&self) -> bool {\n*231|        !self.broker.any_local_controller_wedged()\n 232|    }\n 233|}\n 234|\n...\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...\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"}],"details":{"scopePath":"crates/spt-daemon/src/brain.rs, crates/spt-daemon/src/brainproc.rs, crates/spt-daemon/tests/brain_resume_conn_deadlock.rs, crates/spt-daemon/tests/false_promote.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":62,"fileCount":4,"files":["crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/false_promote.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brain.rs","count":15},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":20},{"path":"crates/spt-daemon/tests/false_promote.rs","count":7}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n\n## src/\n### brain.rs\n   76│/// respawns reconstructs continuity by querying the broker\n  *77│/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n   78│/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n   79│/// solely because the integration tests (separate test target — `#[cfg(test)]`\n   80│/// would hide them) still drive the handoff-frame shape directly; there is **no\n     │...\n  282│    /// continuity state a cold-started brain rebuilds from the broker via\n *283│    /// [`Brain::resume_sessions`], replacing the retired single-session\n  284│    /// `BrainState` handoff. A session present here is in **resume mode** — its\n  285│    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n  286│    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n     │...\n  380│    /// **Test-only (restoration D4-3).** The production resume path is\n *381│    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n  382│    /// never this brain→brain frame; `handoff` survives only for the integration\n  383│    /// tests. See [`BrainState`].\n  384│    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n     │...\n  389│        // `read_event` routes this session through the dedup-below + snap-above\n *390│        // path (matching `resume_sessions`), not the legacy reject-gap path that\n  391│        // turns any at-least-once boundary reorder into a fatal `output gap`.\n  392│        let mut session_cursors = std::collections::HashMap::new();\n  393│        session_cursors.insert(prior.session_id, prior.next_seq);\n     │...\n  642│                    // A **resume-mode** brain (the per-session map is populated by\n *643│                    // `resume_sessions`) routes EVERY session through the map —\n  644│                    // including a freshly-spawned or first-seen one, which seeds\n  645│                    // itself at its first `seq` via `entry().or_insert` (D4-2b,\n  646│                    // doyle amendment 5: this closes the mixed-mode gap where N≥2\n     │...\n 1036│    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n*1037│    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n 1038│    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n*1039│    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n 1040│    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n 1041│    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n 1042│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n     │...\n 1045│    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*1046│    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n*1047│        let reply = self.sessions()?;\n 1048│        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1049│        for info in reply.sessions {\n 1050│            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n     │...\n 1063│    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n*1064│    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n 1065│    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n 1066│    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n 1067│    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n*1068│    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n 1069│    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n 1070│    /// the observation cursor is all it needs; no attach also means no controller steal\n 1071│    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n     │...\n 1076│    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*1077│    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n*1078│        let reply = self.sessions()?;\n 1079│        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1080│        for info in reply.sessions {\n 1081│            self.session_cursors\n     │...\n 1088│    /// The per-session resume cursor (the next seq still needed for a session\n*1089│    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.\n 1090│    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {\n 1091│        self.session_cursors.get(&session_id).copied()\n 1092│    }\n     │...\n 1605│    /// `spt rc`) it never drains, freezing the session\n*1606│    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL). [`Brain::resume_sessions`] re-attaches\n 1607│    /// as Viewer instead; Control is reserved for sessions the daemon brain drives.\n 1608│    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 1609│    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n### brainproc.rs\n  193│    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n *194│    // this conn (the heartbeat below reads it only in net_status()/sessions()\n  195│    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n  196│    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n *197│    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n  198│    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n *199│    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n  200│    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n  201│    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n *202│    match brain.resume_session_cursors() {\n  203│        Ok(ids) if !ids.is_empty() => {\n  204│            eprintln!(\n  205│                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n     │...\n  220│    // semantic the live-agent adapter inherits.\n *221│    write_ready(generation);\n  222│\n  223│    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n  224│    // online/offline exclusivity — a reconcile loop that runs each offline\n     │...\n  250│    // reconcile in the background (ready-without-warm is preserved because\n *251│    // write_ready already fired above), then drain/debounce/coalesce the\n  252│    // invalidation queue. Same posture as the hosts above: the stop flag is\n  253│    // held for symmetry, the thread dies with the brain, and the next brain\n  254│    // rehydrates from the persisted index (warm start, fingerprint-gated).\n     │...\n  263│        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n *264│        let status = match brain.net_status() {\n  265│            Ok(s) => s,\n  266│            Err(e) => {\n  267│                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n     │...\n  285│        // window, so without this the wedge never drains: the promotion DRAINED gate\n *286│        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n  287│        // drives the evict) stays false for the full trial → \"alive but never ready\"\n  288│        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n  289│        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n     │...\n  295│        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n *296│        let _ = brain.sessions();\n *297│        write_ready(generation);\n  298│    }\n  299│}\n  300│\n     │...\n  364│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n *365│fn write_ready(generation: u64) {\n  366│    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n  367│    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n  368│    // tolerates its absence). Never fail the ready write over a diagnostic field.\n     │...\n  481│    /// strands an apply where no old-gen control plane exists.\n *482│    fn old_gen_drained(&self) -> bool;\n  483│}\n  484│\n  485│/// The no-trial environment: every probe degrades to \"nothing pending / not\n     │...\n  505│    }\n *506│    fn old_gen_drained(&self) -> bool {\n  507│        // No readiness gate to drive here → no old-gen control plane to drain; the\n  508│        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n  509│        // no-op effects).\n     │...\n  519│/// Holds the live [`crate::broker::Broker`] handle (or `None` on a net-less /\n *520│/// bind-failed daemon) so the DRAINED gate ([`TrialEnv::old_gen_drained`]) reads\n  521│/// broker truth IN-PROCESS — the supervisor thread runs inside the broker process,\n  522│/// so this is a direct memory read, never an IPC round-trip through a possibly-\n  523│/// wedged conn (REQ-UPDATE-PROMOTE-DRAINED).\n     │...\n  571│    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n *572│    fn old_gen_drained(&self) -> bool {\n  573│        // Drained ⟺ no hosted session has a wedged LOCAL (brain-own) controller conn.\n  574│        // No broker (net-less / bind-failed) → no control plane → drained. A broker\n  575│        // with no wedged local controller (the common case, incl. no daemon-hosted\n     │...\n  578│            .as_ref()\n *579│            .is_none_or(|b| !b.any_local_controller_wedged())\n  580│    }\n  581│}\n  582│\n     │...\n  671│/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n *672│/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n  673│/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n  674│/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n  675│/// the candidate and rolls back (conservative: never a false-promote onto a wedge).\n     │...\n  697│        }\n *698│        if ready_seen && env.old_gen_drained() {\n  699│            return TrialStep::Promoted;\n  700│        }\n  701│        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n     │...\n 1457│        staged_hash: Mutex<Option<String>>,\n*1458│        // DRAINED-gate countdown (REQ-UPDATE-PROMOTE-DRAINED): each `old_gen_drained`\n 1459│        // call returns `false` and decrements while > 0, then `true`. 0 (the default)\n 1460│        // = always drained, so every pre-existing trial test promotes exactly as\n 1461│        // before; `u32::MAX` = never drains within a test window; a small N = drains\n     │...\n 1492│\n*1493│        /// Arm the DRAINED gate: `old_gen_drained` returns `false` for the first\n 1494│        /// `polls` calls, then `true` (0 = always drained; `u32::MAX` = never within\n 1495│        /// the test window). Models the OLD-gen control plane draining after N polls.\n 1496│        fn drains_after(self, polls: u32) -> Self {\n\n## tests/\n### brain_resume_conn_deadlock.rs\n   4│//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n  *5│//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n   6│//! onto the brain's OWN request/reply conn. A conn's send half is one\n   7│//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n   8│//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n    │...\n  10│//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n *11│//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n  12│//! calls. When an actively-streaming session backs the conn up, the brain's own\n  13│//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n  14│//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n    │...\n  19│//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n *20│//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n *21│//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n  22│//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n  23│//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n  24│//! a truly deadlocked one completes ~zero).\n    │...\n 133│/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134│/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n*135│/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n 136│/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n 137│#[derive(Clone, Copy)]\n 138│enum ResumeMode {\n    │...\n 142│/// Drive `run_brain`'s heartbeat pattern on a fresh `Brain` conn: resume per `mode`,\n*143│/// then loop `net_status()` + `sessions()` — the exact per-tick IPC — incrementing a\n 144│/// shared counter per COMPLETED tick, so the caller can measure liveness in a window.\n 145│/// The probe NEVER drains subscribed output (it only issues request/reply, exactly like\n 146│/// the real non-draining daemon heartbeat) — so `CursorOnly` faithfully models the\n    │...\n 162│            ResumeMode::CursorOnly => {\n*163│                let _ = brain.resume_session_cursors();\n 164│            }\n 165│        }\n 166│        while !stop.load(Ordering::Relaxed) {\n 167│            // One heartbeat tick = the two IPC round-trips run_brain issues.\n*168│            if brain.net_status().is_err() {\n 169│                return;\n 170│            }\n*171│            if brain.sessions().is_err() {\n 172│                return;\n 173│            }\n 174│            ticks.fetch_add(1, Ordering::Relaxed);\n    │...\n 216│/// STEADY-STATE severity — the SHIPPING path: the daemon brain resumes CURSOR-ONLY\n*217│/// (`resume_session_cursors`, no subscribe) while N sessions actively stream, so its\n 218│/// heartbeat stays live. NO dead peer anywhere. RED-first: revert run_brain to the\n*219│/// subscribing `resume_sessions` (or see the regression-guard test below) and this\n 220│/// starves to a standstill.\n 221│// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 222│#[test]\n    │...\n 265│/// Two settled arms, no timing window:\n*266│///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n 267│///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268│///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269│///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n    │...\n 271│///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\n*272│///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n 273│///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\n 274│///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\n 275│///   against an always-0 seam).\n    │...\n 278│/// (it writes the `KIND_SUBSCRIBE` frame and does NOT await `KIND_SUBSCRIBED`), and\n*279│/// `resume_sessions` runs its internal `sessions()` round-trip BEFORE sending those\n 280│/// subscribes — so on return the subscribes may not yet be applied. We force a\n*281│/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\n 282│/// the broker dispatches that conn's frames strictly in order and inserts each viewer\n 283│/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\n 284│/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\n    │...\n 287│/// retire/evict a viewer before we read the counts. State is settled the moment the\n*288│/// barrier `sessions()` returns.\n 289│// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n 290│#[test]\n 291│fn daemon_resume_leaves_zero_brain_subscribers() {\n    │...\n 315│    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\n*316│    brain.resume_session_cursors().expect(\"cursor-only resume\");\n 317│    for &sid in &sids {\n 318│        assert_eq!(\n 319│            broker.test_session_viewer_count(sid),\n    │...\n 329│    // SEAM-SENSITIVITY arm — a SECOND brain on the SAME broker takes the hazard path\n*330│    // (`resume_sessions` still exists) and MUST raise every count to >= 1, proving the\n 331│    // seam actually observes the subscription the positive arm forbids (else the Some(0)\n 332│    // above is a vacuous pass against an always-0 read).\n 333│    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\n*334│    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\n### false_promote.rs\n  13│//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n *14│//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n  15│//! read from BROKER TRUTH in-process (no brain round-trip).\n  16│//!\n  17│//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n    │...\n  40│//! 1. the trial does NOT promote while wedged (RED-first: remove the\n *41│//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n  42│//!    reproducing the 22:47 false-promote);\n  43│//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n  44│//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n    │...\n 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 229│    // The gate under test: REAL broker truth, no brain round-trip.\n*230│    fn old_gen_drained(&self) -> bool {\n*231│        !self.broker.any_local_controller_wedged()\n 232│    }\n 233│}\n 234│\n    │...\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    │...\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"},"isError":false,"timestamp":1784195796292}}
{"type":"message","id":"91b37228","parentId":"205922ef","timestamp":"2026-07-16T09:56:36.296Z","message":{"role":"toolResult","toolCallId":"call_pNsUeuhVUw4Lzy0omddKVAUY|fc_0cc7a0c9a797f232016a58aad4353c819ab980f3a6a89e84fc","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\nwake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim.\nInt: two concurrent wake requests → exactly one launch tree.\n\n**REQ-HAZARD-LISTEN-ORPHAN** (impl, unit)\nRoot (mobile-gw RCA): `api listen --parent-pid N` is auth-anchor ONLY — no liveness watch;\nhost death orphans the listener forever → perch held alive (false ONLINE), EVENTs stream to a\ndead stdout, dead-owner rebind BLOCKED (recorded pid = the live orphan). Fix: listener watches\n`--parent-pid` liveness (Windows: job object or poll; Unix: PDEATHSIG or poll) and exits loud\non parent death. flynn's job-object guard (spt-mobile side) stays regardless; filed\nSPT-CORE-NEEDS §5.\nUnit: parent-death → listener exits within one poll window.\n\n## W5 — delivery integrity\n\n> **Two-root split (doyle, 2026-07-07 — both live-reproduced tonight, distinct roots).**\n> The night's delivery failures resolve into TWO separate defects, one per REQ below:\n> **(a) parked-idle sleep** = REQ-IDLE-PARKED-DELIVERY — a QUEUED message to an idle\n> endpoint PARKS (not lost) and flushes only when inbound activity wakes the target's\n> delivery drain. Confirmed clean wake trigger: todlando's W2-proposal parts (part-1\n> tail + 3/4) sat parked at doyle's perch for HOURS, then flushed the instant perri's\n> idle-inject probe woke doyle's drain — parked, not truncated. **(b) live-SENT\n> truncation/empty-body** = REQ-INJECT-MULTILINE-INTEGRITY (below) — a genuinely\n> mangled payload on the typed leg, unrecoverable (no spool copy). Do NOT conflate:\n> (a) is a scheduling/wake gap (bytes intact, late); (b) is byte corruption in transit.\n> File-transport workaround (proposals/reports ride branch files, sends carry only the\n> pointer) dodges BOTH legs and stays binding this milestone.\n\n**REQ-INJECT-MULTILINE-INTEGRITY** (impl, unit, int)\n> **ROOT PINNED (doyle + perri, 2026-07-07 — pre-activation).** Perri's 4-point\n> verbatim series pinned it to a RAW TYPED-STREAM byte window: an inbound envelope\n> over `EVENT_LINE_THRESHOLD` (400B, `chunk.rs`) is split into `EVENT-PART` lines;\n> the TERMINAL-SIDE pre-settle window swallows a raw byte span of ~1148–1152B\n> (incl. line framing) and cuts MID part-3 body — a ~20B tail survives, which is\n> exactly why every artifact starts mid-word. Slot math verified on perri's verbatim\n> `seq=1/4` line: first slot is exactly 321B, attrs `seq`/`id`/`type`/`from` only.\n> Constant escaped head-loss across the series (992/990/988/992B). NOT a size cap and\n> NOT the parked-idle wake gap (that is REQ-IDLE-PARKED-DELIVERY) — this is a\n> pre-settle byte-window swallow on the typed leg. **Fix direction:** instrument the\n> inject pre-settle window; settle-before-type or ack-paced parts (chunk-then-settle\n> per EVENT-PART, or a per-part write+echo-verify). **Interim protocol (fleet-wide,\n> binding this milestone):** sub-400B sends (unchunked, ride one slot) OR file\n> transport; anything larger risks the swallow. **Open question (doyle):** the WIRE\n> first-part attrs run ~20B longer than the on-paper count — the raw relay-log line\n> will settle the exact slot math before the fix encodes a threshold.\n>\n> (Field instances this milestone, all live-SENT, no spool copy, unrecoverable:\n> todlando's W1 gate report HEAD-TRUNCATED at doyle; a flynn EVENT fully EMPTY.)\n\nRoot: the idle-inject TYPED delivery leg eats HEAD bytes\nnondeterministically — spool rows complete (1669B) vs ~322B received suffix; mid-turn poll\nenvelopes always intact; a 1854B body later rode the same leg intact ⇒ timing race\n(terminal-readiness / enter-coalescing settle class), not a size cap. Fix direction (todlando\nproposes on the broker/translate typed-inject seam): settle-before-head, bracketed-paste where\nthe harness supports it, or chunked write with echo-verify. Note: live-SENT injects leave NO\nspool copy — truncation there is unrecoverable, which raises the stakes.\nInt: repeated large multi-line injects into a real PTY session arrive byte-complete (loop N\ntimes — the race is timing-dependent, single-shot green is not proof).\n\n**REQ-IDLE-PARKED-DELIVERY** (impl, unit, int)\nRoot (live during this very dispatch, 2026-07-07): a message QUEUED to an ALREADY-idle\nendpoint sleeps indefinitely — the idle-edge drain (F-023 leg 2) fires only on the\nACTIVE→IDLE transition, and no new edge ever comes for a parked session; the send-time inject\ndidn't carry it either (both doyle→todlando milestone dispatches sat delivered=0 in the spool\nwhile the endpoint showed ONLINE). Fix: send-time inject must fire for an already-idle\nspt-hosted target (the activity sense says idle ⇒ inject now, not spool), and/or a bounded\nspool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop).\nInt: send to a session idle for N minutes → delivered without any operator poke.\nLive confirmation (2026-07-07): parked messages flush the moment inbound activity wakes\nthe target's delivery drain (doyle's parked W2-proposal parts flushed on perri's\nidle-inject probe) — the wake trigger is real, so the fix is \"make the send-time inject\nfire for an already-idle target\" / \"sweep on the pulse tick\", not a new wake mechanism.\n\n**REQ-SPOOL-TAKE-AUDIT** (impl, unit)\nRCA cost: proving WHO took delivered=1 rows burned an hour. Fix: spool records taker per row\n(leg enum: relay-backlog / hook-poll / idle-inject / psyche, + sid/pid + taken_at ms); surfaced\nby a `--json` debug read. Additive column, no schema break (delivered rows already retained).\n\n## W6 — docs wave (publish the contracts perri built blind against)\n\n**REQ-DOC-ECHO-COMMUNE-CONTRACT** (doc)\n`[session.echo_commune]` I/O contract on the published docs-site: key catalog core fills; core\ndoes NOT stdin-feed [history] (field-proven); self-locate guidance incl. CLAUDE_CONFIG_DIR /\nread_env; drop-file protocol (single-writer, ingest-deletes, resolver semantics from W1);\nstdout ingestion expectations. This gap cost a full outage night — priority slot.\n\n**REQ-DOC-DELIVERY-VOCAB** (doc)\nThe remaining flynn/perri gaps folded: full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED\nwindow semantics / DEFERRED / NO_PERCH), `digest --json` row schema, api poll auth + MAC-stamp\nprefix, remaining `--json` shapes checklist (seed #3). Public docs use VERSION numbers, never\nwave codes; docs-publish drift gate applies.\n\n---\n\n## Sequencing + gate notes\n\n- W1 ships alone and fast (three small REQs, one seam) — unblocks two live psyches; doyle\n  field-accepts on this node same-day.\n- W2 needs a doyle design ruling on the decouple shape BEFORE impl (todlando proposes; the\n  log-lock/subscriber contract is KNOWN-HAZARDS-grade). Its int test IS tonight's rig.\n- W3 composes: FINISH rides ENDPOINT-SURVIVAL. RC-RECONNECT-TRUTH is separable if the wave\n  runs long.\n- W4/W5 independent of W2/W3 — parallelizable if a second executor frees up, else sequential.\n- Every wave: traceable activate → build → `traceable-reqs check` exit-0 → doyle gate\n  (worktree of the wave sha) → next wave. Release cut at milestone end per RELEASE-RUNBOOK\n  (bump-ON-milestone-branch pattern, deployah drives).\n"}],"details":{"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\nwake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim.\r\nInt: two concurrent wake requests → exactly one launch tree.\r\n\r\n**REQ-HAZARD-LISTEN-ORPHAN** (impl, unit)\r\nRoot (mobile-gw RCA): `api listen --parent-pid N` is auth-anchor ONLY — no liveness watch;\r\nhost death orphans the listener forever → perch held alive (false ONLINE), EVENTs stream to a\r\ndead stdout, dead-owner rebind BLOCKED (recorded pid = the live orphan). Fix: listener watches\r\n`--parent-pid` liveness (Windows: job object or poll; Unix: PDEATHSIG or poll) and exits loud\r\non parent death. flynn's job-object guard (spt-mobile side) stays regardless; filed\r\nSPT-CORE-NEEDS §5.\r\nUnit: parent-death → listener exits within one poll window.\r\n\r\n## W5 — delivery integrity\r\n\r\n> **Two-root split (doyle, 2026-07-07 — both live-reproduced tonight, distinct roots).**\r\n> The night's delivery failures resolve into TWO separate defects, one per REQ below:\r\n> **(a) parked-idle sleep** = REQ-IDLE-PARKED-DELIVERY — a QUEUED message to an idle\r\n> endpoint PARKS (not lost) and flushes only when inbound activity wakes the target's\r\n> delivery drain. Confirmed clean wake trigger: todlando's W2-proposal parts (part-1\r\n> tail + 3/4) sat parked at doyle's perch for HOURS, then flushed the instant perri's\r\n> idle-inject probe woke doyle's drain — parked, not truncated. **(b) live-SENT\r\n> truncation/empty-body** = REQ-INJECT-MULTILINE-INTEGRITY (below) — a genuinely\r\n> mangled payload on the typed leg, unrecoverable (no spool copy). Do NOT conflate:\r\n> (a) is a scheduling/wake gap (bytes intact, late); (b) is byte corruption in transit.\r\n> File-transport workaround (proposals/reports ride branch files, sends carry only the\r\n> pointer) dodges BOTH legs and stays binding this milestone.\r\n\r\n**REQ-INJECT-MULTILINE-INTEGRITY** (impl, unit, int)\r\n> **ROOT PINNED (doyle + perri, 2026-07-07 — pre-activation).** Perri's 4-point\r\n> verbatim series pinned it to a RAW TYPED-STREAM byte window: an inbound envelope\r\n> over `EVENT_LINE_THRESHOLD` (400B, `chunk.rs`) is split into `EVENT-PART` lines;\r\n> the TERMINAL-SIDE pre-settle window swallows a raw byte span of ~1148–1152B\r\n> (incl. line framing) and cuts MID part-3 body — a ~20B tail survives, which is\r\n> exactly why every artifact starts mid-word. Slot math verified on perri's verbatim\r\n> `seq=1/4` line: first slot is exactly 321B, attrs `seq`/`id`/`type`/`from` only.\r\n> Constant escaped head-loss across the series (992/990/988/992B). NOT a size cap and\r\n> NOT the parked-idle wake gap (that is REQ-IDLE-PARKED-DELIVERY) — this is a\r\n> pre-settle byte-window swallow on the typed leg. **Fix direction:** instrument the\r\n> inject pre-settle window; settle-before-type or ack-paced parts (chunk-then-settle\r\n> per EVENT-PART, or a per-part write+echo-verify). **Interim protocol (fleet-wide,\r\n> binding this milestone):** sub-400B sends (unchunked, ride one slot) OR file\r\n> transport; anything larger risks the swallow. **Open question (doyle):** the WIRE\r\n> first-part attrs run ~20B longer than the on-paper count — the raw relay-log line\r\n> will settle the exact slot math before the fix encodes a threshold.\r\n>\r\n> (Field instances this milestone, all live-SENT, no spool copy, unrecoverable:\r\n> todlando's W1 gate report HEAD-TRUNCATED at doyle; a flynn EVENT fully EMPTY.)\r\n\r\nRoot: the idle-inject TYPED delivery leg eats HEAD bytes\r\nnondeterministically — spool rows complete (1669B) vs ~322B received suffix; mid-turn poll\r\nenvelopes always intact; a 1854B body later rode the same leg intact ⇒ timing race\r\n(terminal-readiness / enter-coalescing settle class), not a size cap. Fix direction (todlando\r\nproposes on the broker/translate typed-inject seam): settle-before-head, bracketed-paste where\r\nthe harness supports it, or chunked write with echo-verify. Note: live-SENT injects leave NO\r\nspool copy — truncation there is unrecoverable, which raises the stakes.\r\nInt: repeated large multi-line injects into a real PTY session arrive byte-complete (loop N\r\ntimes — the race is timing-dependent, single-shot green is not proof).\r\n\r\n**REQ-IDLE-PARKED-DELIVERY** (impl, unit, int)\r\nRoot (live during this very dispatch, 2026-07-07): a message QUEUED to an ALREADY-idle\r\nendpoint sleeps indefinitely — the idle-edge drain (F-023 leg 2) fires only on the\r\nACTIVE→IDLE transition, and no new edge ever comes for a parked session; the send-time inject\r\ndidn't carry it either (both doyle→todlando milestone dispatches sat delivered=0 in the spool\r\nwhile the endpoint showed ONLINE). Fix: send-time inject must fire for an already-idle\r\nspt-hosted target (the activity sense says idle ⇒ inject now, not spool), and/or a bounded\r\nspool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop).\r\nInt: send to a session idle for N minutes → delivered without any operator poke.\r\nLive confirmation (2026-07-07): parked messages flush the moment inbound activity wakes\r\nthe target's delivery drain (doyle's parked W2-proposal parts flushed on perri's\r\nidle-inject probe) — the wake trigger is real, so the fix is \"make the send-time inject\r\nfire for an already-idle target\" / \"sweep on the pulse tick\", not a new wake mechanism.\r\n\r\n**REQ-SPOOL-TAKE-AUDIT** (impl, unit)\r\nRCA cost: proving WHO took delivered=1 rows burned an hour. Fix: spool records taker per row\r\n(leg enum: relay-backlog / hook-poll / idle-inject / psyche, + sid/pid + taken_at ms); surfaced\r\nby a `--json` debug read. Additive column, no schema break (delivered rows already retained).\r\n\r\n## W6 — docs wave (publish the contracts perri built blind against)\r\n\r\n**REQ-DOC-ECHO-COMMUNE-CONTRACT** (doc)\r\n`[session.echo_commune]` I/O contract on the published docs-site: key catalog core fills; core\r\ndoes NOT stdin-feed [history] (field-proven); self-locate guidance incl. CLAUDE_CONFIG_DIR /\r\nread_env; drop-file protocol (single-writer, ingest-deletes, resolver semantics from W1);\r\nstdout ingestion expectations. This gap cost a full outage night — priority slot.\r\n\r\n**REQ-DOC-DELIVERY-VOCAB** (doc)\r\nThe remaining flynn/perri gaps folded: full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED\r\nwindow semantics / DEFERRED / NO_PERCH), `digest --json` row schema, api poll auth + MAC-stamp\r\nprefix, remaining `--json` shapes checklist (seed #3). Public docs use VERSION numbers, never\r\nwave codes; docs-publish drift gate applies.\r\n\r\n---\r\n\r\n## Sequencing + gate notes\r\n\r\n- W1 ships alone and fast (three small REQs, one seam) — unblocks two live psyches; doyle\r\n  field-accepts on this node same-day.\r\n- W2 needs a doyle design ruling on the decouple shape BEFORE impl (todlando proposes; the\r\n  log-lock/subscriber contract is KNOWN-HAZARDS-grade). Its int test IS tonight's rig.\r\n- W3 composes: FINISH rides ENDPOINT-SURVIVAL. RC-RECONNECT-TRUTH is separable if the wave\r\n  runs long.\r\n- W4/W5 independent of W2/W3 — parallelizable if a second executor frees up, else sequential.\r\n- Every wave: traceable activate → build → `traceable-reqs check` exit-0 → doyle gate\r\n  (worktree of the wave sha) → next wave. Release cut at milestone end per RELEASE-RUNBOOK\r\n  (bump-ON-milestone-branch pattern, deployah drives).\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md"}}},"isError":false,"timestamp":1784195796296}}
{"type":"message","id":"43b12063","parentId":"91b37228","timestamp":"2026-07-16T09:56:36.299Z","message":{"role":"toolResult","toolCallId":"call_pbxWzMwgk5wGSgnhsrlvg14c|fc_0cc7a0c9a797f232016a58aad43558819aae02ba74599c9635","toolName":"read","content":[{"type":"text","text":"//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\n//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n//!\n//! ## The bug this gate locks down (the 22:47 false-promote)\n//!\n//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\n//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\n//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n//! controller conn is still wedged (blocked writes pending), so `update apply`\n//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n//! read from BROKER TRUTH in-process (no brain round-trip).\n//!\n//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n//!\n//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n//! `write_frame` on a black-holed spawner conn. Round-4\n//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n//! deadline and the rig's persistent-wedge precondition cannot exist (that is the\n//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\n//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n//! is seeded past the deadline and whose writer never touches the socket (no\n//! watchdog armed → the injected wedge persists, zero timing race). The physical\n//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n//! wall-clock only; it never relaxes an assertion).\n//!\n//! ## What this rig asserts (the DRAINED invariant)\n//!\n//! With a ready candidate and the old-gen local controller wedged:\n//! 1. the trial does NOT promote while wedged (RED-first: remove the\n//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n//!    reproducing the 22:47 false-promote);\n//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n//!    across the undrained wait, never a premature rollback.\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 false_promote` (process-per-test — CI's path) or\n//! `cargo test --test false_promote -- --test-threads=1`. Never a bare\n//! `cargo test --test false_promote` on Windows.\n\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, AtomicU32, Ordering};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::brainproc::{supervise_brain, BrainRestart, TrialEnv};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    SpawnReq, Spawned, SubscribeReq, SubscribedReply, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\n    KIND_SUBSCRIBED,\n};\nuse spt_daemon::relcache::AppliedRecord;\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-falsepromote-{}-{}.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 _ = Command::new(\"taskkill\")\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 FLOODS stdout forever (never reads stdin) — fills a non-reading\n/// controller's socket buffer and parks its broker-side writer (the wedge mechanism).\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\n/// A long-lived trial-candidate child (NOT a real brain — the rig fakes the readiness\n/// stamp + the drained truth; the candidate only has to stay alive through the trial).\nfn candidate_child() -> std::io::Result<Child> {\n    #[cfg(windows)]\n    {\n        Command::new(\"cmd\")\n            .args([\"/C\", \"ping\", \"-n\", \"60\", \"127.0.0.1\"])\n            .stdout(std::process::Stdio::null())\n            .spawn()\n    }\n    #[cfg(unix)]\n    {\n        Command::new(\"sleep\").arg(\"60\").spawn()\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/// A DIFFERENT operator's Control subscribe (remote, `by = Some`) — the take that\n/// stall-evicts the wedged local incumbent broker-side (the W2 self-heal). Holds the\n/// conn briefly so the take is not immediately detached, then lets it fall.\nfn remote_take(name: &str, sid: u64) {\n    let name = name.to_string();\n    thread::spawn(move || {\n        let mut c = connect(&name);\n        send(\n            &mut c,\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-two\".to_string()),\n            })\n            .unwrap(),\n        );\n        loop {\n            match read_frame(&mut c) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => {\n                    let _: SubscribedReply = serde_json::from_value(f.payload).unwrap();\n                    thread::sleep(Duration::from_millis(400));\n                    return;\n                }\n                Ok(_) => continue,\n                Err(_) => return,\n            }\n        }\n    });\n}\n\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial with a faked readiness\n/// stamp, but the DRAINED gate reads REAL broker truth (`any_local_controller_wedged`)\n/// off the live broker — the whole point of the rig. `record_promoted` captures the\n/// version AND raises `stop` so `supervise_brain` returns.\nstruct FalsePromoteEnv {\n    broker: Arc<Broker>,\n    ready: Mutex<Option<u64>>,\n    promotions: Mutex<Vec<u64>>,\n    rollbacks: Mutex<Vec<u64>>,\n    stop: Arc<AtomicBool>,\n}\n\nimpl TrialEnv for FalsePromoteEnv {\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        *self.ready.lock().unwrap()\n    }\n    fn clear_ready(&self) {\n        *self.ready.lock().unwrap() = None;\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 test: 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// [int->REQ-UPDATE-PROMOTE-DRAINED]\n#[test]\nfn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\n    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\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 LOCAL\n    //    controller (by = None) — the old-generation-conn identity. ──\n    let mut a = connect(&name);\n    send(\n        &mut a,\n        KIND_SPAWN,\n        serde_json::to_value(flood_spawn_req(\"falsepromote-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    // Inject the statically-held wedge (doyle Option C): a LOCAL controller whose\n    // write_blocked_since is already 1 s in the past (> the 400 ms deadline) and\n    // whose writer never touches the socket — the round-4 watchdog has nothing to\n    // cancel, so the wedge persists until the take drains it logically. This\n    // REPLACES A's spawn-attached controller sink; A itself can drop (its conn\n    // detach no longer owns the controller slot).\n    assert!(\n        broker.test_seed_wedged_local_controller(sid, 1_000),\n        \"precondition: the wedge seed must install on the hosted session\"\n    );\n    drop(a);\n\n    // Broker truth reads WEDGED immediately — the seed is already past-deadline\n    // (no maturation sleep, no race against the round-4 watchdog).\n    assert!(\n        broker.any_local_controller_wedged(),\n        \"precondition: the old-gen local controller conn must read WEDGED before the trial\"\n    );\n\n    // ── Drive the REAL promotion path (supervise_brain → run_trial) with a scripted\n    //    AppliedPending trial whose candidate signals ready but whose DRAINED gate reads\n    //    the live wedged broker. ──\n    let stop = Arc::new(AtomicBool::new(false));\n    let env = Arc::new(FalsePromoteEnv {\n        broker: Arc::clone(&broker),\n        ready: Mutex::new(None),\n        promotions: Mutex::new(Vec::new()),\n        rollbacks: Mutex::new(Vec::new()),\n        stop: Arc::clone(&stop),\n    });\n    let env_sup = Arc::clone(&env);\n    let stop_sup = Arc::clone(&stop);\n    let stop_spawn = Arc::clone(&stop);\n    let ready_spawn = Arc::clone(&env);\n    let sup = thread::spawn(move || {\n        let restart = BrainRestart::new();\n        supervise_brain(\n            &stop_sup,\n            &restart,\n            Duration::from_millis(5),\n            env_sup.as_ref(),\n            Duration::from_secs(20), // generous window — the take + drain arrive well inside it\n            move |gen, _reason, binary| {\n                if binary.is_some() {\n                    // A rollback spawn would mean the trial FAILED to promote — end the\n                    // run so the test surfaces the empty `promotions` rather than hanging.\n                    stop_spawn.store(true, Ordering::Relaxed);\n                    return candidate_child();\n                }\n                // The candidate signals ready for its generation, then stays alive.\n                *ready_spawn.ready.lock().unwrap() = Some(gen);\n                candidate_child()\n            },\n        );\n    });\n\n    // Several ticks past the shrunk deadline: ready is stamped, but the old-gen conn is\n    // still wedged → the trial MUST NOT have promoted (RED-first: without the drained\n    // gate it promotes here on ready alone — the 22:47 false-promote).\n    thread::sleep(Duration::from_millis(1200));\n    assert!(\n        env.promotions.lock().unwrap().is_empty(),\n        \"a ready candidate must NOT promote while the OLD generation's conn is still wedged\"\n    );\n    assert!(\n        env.rollbacks.lock().unwrap().is_empty(),\n        \"and it must not have rolled back yet — the window is far from elapsed\"\n    );\n\n    // ── Drain the old-gen conn: a second operator's take stall-evicts the wedged local\n    //    incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. ──\n    remote_take(&name, sid);\n\n    // Poll for the promotion the drain unblocks (bounded — the gate flips within a tick\n    // of the evict; a stall-evict is prompt, not a hang).\n    let deadline = Instant::now() + Duration::from_secs(8);\n    let promoted = loop {\n        if !env.promotions.lock().unwrap().is_empty() {\n            break true;\n        }\n…\n    };\n…\n}\n\n[Showing lines 1-343 of 376. Use :344 to continue]"}],"details":{"truncation":{"content":"//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\r\n//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\r\n//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\r\n//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\r\n//!\r\n//! ## The bug this gate locks down (the 22:47 false-promote)\r\n//!\r\n//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\r\n//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\r\n//! can signal `brain.ready` INSIDE that window while the OLD generation's local\r\n//! controller conn is still wedged (blocked writes pending), so `update apply`\r\n//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\r\n//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\r\n//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\r\n//! read from BROKER TRUTH in-process (no brain round-trip).\r\n//!\r\n//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\r\n//!\r\n//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\r\n//! `write_frame` on a black-holed spawner conn. Round-4\r\n//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\r\n//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\r\n//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\r\n//! deadline and the rig's persistent-wedge precondition cannot exist (that is the\r\n//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\r\n//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\r\n//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\r\n//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\r\n//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\r\n//! is seeded past the deadline and whose writer never touches the socket (no\r\n//! watchdog armed → the injected wedge persists, zero timing race). The physical\r\n//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\r\n//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\r\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\r\n//! wall-clock only; it never relaxes an assertion).\r\n//!\r\n//! ## What this rig asserts (the DRAINED invariant)\r\n//!\r\n//! With a ready candidate and the old-gen local controller wedged:\r\n//! 1. the trial does NOT promote while wedged (RED-first: remove the\r\n//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\r\n//!    reproducing the 22:47 false-promote);\r\n//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\r\n//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\r\n//!    across the undrained wait, never a premature rollback.\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 false_promote` (process-per-test — CI's path) or\r\n//! `cargo test --test false_promote -- --test-threads=1`. Never a bare\r\n//! `cargo test --test false_promote` on Windows.\r\n\r\nuse std::process::{Child, Command};\r\nuse std::sync::atomic::{AtomicBool, AtomicU32, Ordering};\r\nuse std::sync::{Arc, Mutex, OnceLock};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse interprocess::local_socket::Stream;\r\nuse spt_daemon::brainproc::{supervise_brain, BrainRestart, TrialEnv};\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, SubscribeReq, SubscribedReply, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\r\n    KIND_SUBSCRIBED,\r\n};\r\nuse spt_daemon::relcache::AppliedRecord;\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-falsepromote-{}-{}.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 _ = Command::new(\"taskkill\")\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 FLOODS stdout forever (never reads stdin) — fills a non-reading\r\n/// controller's socket buffer and parks its broker-side writer (the wedge mechanism).\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\n/// A long-lived trial-candidate child (NOT a real brain — the rig fakes the readiness\r\n/// stamp + the drained truth; the candidate only has to stay alive through the trial).\r\nfn candidate_child() -> std::io::Result<Child> {\r\n    #[cfg(windows)]\r\n    {\r\n        Command::new(\"cmd\")\r\n            .args([\"/C\", \"ping\", \"-n\", \"60\", \"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(\"60\").spawn()\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/// A DIFFERENT operator's Control subscribe (remote, `by = Some`) — the take that\r\n/// stall-evicts the wedged local incumbent broker-side (the W2 self-heal). Holds the\r\n/// conn briefly so the take is not immediately detached, then lets it fall.\r\nfn remote_take(name: &str, sid: u64) {\r\n    let name = name.to_string();\r\n    thread::spawn(move || {\r\n        let mut c = connect(&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: 0,\r\n                intent: AttachIntent::Control,\r\n                by: Some(\"operator-two\".to_string()),\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 _: SubscribedReply = serde_json::from_value(f.payload).unwrap();\r\n                    thread::sleep(Duration::from_millis(400));\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => return,\r\n            }\r\n        }\r\n    });\r\n}\r\n\r\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial with a faked readiness\r\n/// stamp, but the DRAINED gate reads REAL broker truth (`any_local_controller_wedged`)\r\n/// off the live broker — the whole point of the rig. `record_promoted` captures the\r\n/// version AND raises `stop` so `supervise_brain` returns.\r\nstruct FalsePromoteEnv {\r\n    broker: Arc<Broker>,\r\n    ready: Mutex<Option<u64>>,\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 FalsePromoteEnv {\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        *self.ready.lock().unwrap()\r\n    }\r\n    fn clear_ready(&self) {\r\n        *self.ready.lock().unwrap() = None;\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 test: 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// [int->REQ-UPDATE-PROMOTE-DRAINED]\r\n#[test]\r\nfn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\r\n    init_home();\r\n    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\r\n    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\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 LOCAL\r\n    //    controller (by = None) — the old-generation-conn identity. ──\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(\"falsepromote-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    // Inject the statically-held wedge (doyle Option C): a LOCAL controller whose\r\n    // write_blocked_since is already 1 s in the past (> the 400 ms deadline) and\r\n    // whose writer never touches the socket — the round-4 watchdog has nothing to\r\n    // cancel, so the wedge persists until the take drains it logically. This\r\n    // REPLACES A's spawn-attached controller sink; A itself can drop (its conn\r\n    // detach no longer owns the controller slot).\r\n    assert!(\r\n        broker.test_seed_wedged_local_controller(sid, 1_000),\r\n        \"precondition: the wedge seed must install on the hosted session\"\r\n    );\r\n    drop(a);\r\n\r\n    // Broker truth reads WEDGED immediately — the seed is already past-deadline\r\n    // (no maturation sleep, no race against the round-4 watchdog).\r\n    assert!(\r\n        broker.any_local_controller_wedged(),\r\n        \"precondition: the old-gen local controller conn must read WEDGED before the trial\"\r\n    );\r\n\r\n    // ── Drive the REAL promotion path (supervise_brain → run_trial) with a scripted\r\n    //    AppliedPending trial whose candidate signals ready but whose DRAINED gate reads\r\n    //    the live wedged broker. ──\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let env = Arc::new(FalsePromoteEnv {\r\n        broker: Arc::clone(&broker),\r\n        ready: Mutex::new(None),\r\n        promotions: Mutex::new(Vec::new()),\r\n        rollbacks: Mutex::new(Vec::new()),\r\n        stop: Arc::clone(&stop),\r\n    });\r\n    let env_sup = Arc::clone(&env);\r\n    let stop_sup = Arc::clone(&stop);\r\n    let stop_spawn = Arc::clone(&stop);\r\n    let ready_spawn = Arc::clone(&env);\r\n    let sup = 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(5),\r\n            env_sup.as_ref(),\r\n            Duration::from_secs(20), // generous window — the take + drain arrive well inside it\r\n            move |gen, _reason, binary| {\r\n                if binary.is_some() {\r\n                    // A rollback spawn would mean the trial FAILED to promote — end the\r\n                    // run so the test surfaces the empty `promotions` rather than hanging.\r\n                    stop_spawn.store(true, Ordering::Relaxed);\r\n                    return candidate_child();\r\n                }\r\n                // The candidate signals ready for its generation, then stays alive.\r\n                *ready_spawn.ready.lock().unwrap() = Some(gen);\r\n                candidate_child()\r\n            },\r\n        );\r\n    });\r\n\r\n    // Several ticks past the shrunk deadline: ready is stamped, but the old-gen conn is\r\n    // still wedged → the trial MUST NOT have promoted (RED-first: without the drained\r\n    // gate it promotes here on ready alone — the 22:47 false-promote).\r\n    thread::sleep(Duration::from_millis(1200));\r\n    assert!(\r\n        env.promotions.lock().unwrap().is_empty(),\r\n        \"a ready candidate must NOT promote while the OLD generation's conn is still wedged\"\r\n    );\r\n    assert!(\r\n        env.rollbacks.lock().unwrap().is_empty(),\r\n        \"and it must not have rolled back yet — the window is far from elapsed\"\r\n    );\r\n\r\n    // ── Drain the old-gen conn: a second operator's take stall-evicts the wedged local\r\n    //    incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. ──\r\n    remote_take(&name, sid);\r\n\r\n    // Poll for the promotion the drain unblocks (bounded — the gate flips within a tick\r\n    // of the evict; a stall-evict is prompt, not a hang).\r\n    let deadline = Instant::now() + Duration::from_secs(8);\r\n    let promoted = loop {\r\n        if !env.promotions.lock().unwrap().is_empty() {\r\n            break true;\r\n        }\r","truncated":true,"truncatedBy":"lines","totalLines":376,"totalBytes":14567,"outputLines":343,"outputBytes":14567,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\r\n//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\r\n//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\r\n//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\r\n//!\r\n//! ## The bug this gate locks down (the 22:47 false-promote)\r\n//!\r\n//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\r\n//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\r\n//! can signal `brain.ready` INSIDE that window while the OLD generation's local\r\n//! controller conn is still wedged (blocked writes pending), so `update apply`\r\n//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\r\n//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\r\n//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\r\n//! read from BROKER TRUTH in-process (no brain round-trip).\r\n//!\r\n//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\r\n//!\r\n//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\r\n//! `write_frame` on a black-holed spawner conn. Round-4\r\n//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\r\n//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\r\n//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\r\n//! deadline and the rig's persistent-wedge precondition cannot exist (that is the\r\n//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\r\n//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\r\n//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\r\n//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\r\n//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\r\n//! is seeded past the deadline and whose writer never touches the socket (no\r\n//! watchdog armed → the injected wedge persists, zero timing race). The physical\r\n//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\r\n//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\r\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\r\n//! wall-clock only; it never relaxes an assertion).\r\n//!\r\n//! ## What this rig asserts (the DRAINED invariant)\r\n//!\r\n//! With a ready candidate and the old-gen local controller wedged:\r\n//! 1. the trial does NOT promote while wedged (RED-first: remove the\r\n//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\r\n//!    reproducing the 22:47 false-promote);\r\n//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\r\n//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\r\n//!    across the undrained wait, never a premature rollback.\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 false_promote` (process-per-test — CI's path) or\r\n//! `cargo test --test false_promote -- --test-threads=1`. Never a bare\r\n//! `cargo test --test false_promote` on Windows.\r\n\r\nuse std::process::{Child, Command};\r\nuse std::sync::atomic::{AtomicBool, AtomicU32, Ordering};\r\nuse std::sync::{Arc, Mutex, OnceLock};\r\nuse std::thread;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse interprocess::local_socket::Stream;\r\nuse spt_daemon::brainproc::{supervise_brain, BrainRestart, TrialEnv};\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, SubscribeReq, SubscribedReply, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\r\n    KIND_SUBSCRIBED,\r\n};\r\nuse spt_daemon::relcache::AppliedRecord;\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-falsepromote-{}-{}.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 _ = Command::new(\"taskkill\")\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 FLOODS stdout forever (never reads stdin) — fills a non-reading\r\n/// controller's socket buffer and parks its broker-side writer (the wedge mechanism).\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\n/// A long-lived trial-candidate child (NOT a real brain — the rig fakes the readiness\r\n/// stamp + the drained truth; the candidate only has to stay alive through the trial).\r\nfn candidate_child() -> std::io::Result<Child> {\r\n    #[cfg(windows)]\r\n    {\r\n        Command::new(\"cmd\")\r\n            .args([\"/C\", \"ping\", \"-n\", \"60\", \"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(\"60\").spawn()\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/// A DIFFERENT operator's Control subscribe (remote, `by = Some`) — the take that\r\n/// stall-evicts the wedged local incumbent broker-side (the W2 self-heal). Holds the\r\n/// conn briefly so the take is not immediately detached, then lets it fall.\r\nfn remote_take(name: &str, sid: u64) {\r\n    let name = name.to_string();\r\n    thread::spawn(move || {\r\n        let mut c = connect(&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: 0,\r\n                intent: AttachIntent::Control,\r\n                by: Some(\"operator-two\".to_string()),\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 _: SubscribedReply = serde_json::from_value(f.payload).unwrap();\r\n                    thread::sleep(Duration::from_millis(400));\r\n                    return;\r\n                }\r\n                Ok(_) => continue,\r\n                Err(_) => return,\r\n            }\r\n        }\r\n    });\r\n}\r\n\r\n/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial with a faked readiness\r\n/// stamp, but the DRAINED gate reads REAL broker truth (`any_local_controller_wedged`)\r\n/// off the live broker — the whole point of the rig. `record_promoted` captures the\r\n/// version AND raises `stop` so `supervise_brain` returns.\r\nstruct FalsePromoteEnv {\r\n    broker: Arc<Broker>,\r\n    ready: Mutex<Option<u64>>,\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 FalsePromoteEnv {\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        *self.ready.lock().unwrap()\r\n    }\r\n    fn clear_ready(&self) {\r\n        *self.ready.lock().unwrap() = None;\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 test: 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// [int->REQ-UPDATE-PROMOTE-DRAINED]\r\n#[test]\r\nfn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\r\n    init_home();\r\n    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\r\n    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\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 LOCAL\r\n    //    controller (by = None) — the old-generation-conn identity. ──\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(\"falsepromote-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    // Inject the statically-held wedge (doyle Option C): a LOCAL controller whose\r\n    // write_blocked_since is already 1 s in the past (> the 400 ms deadline) and\r\n    // whose writer never touches the socket — the round-4 watchdog has nothing to\r\n    // cancel, so the wedge persists until the take drains it logically. This\r\n    // REPLACES A's spawn-attached controller sink; A itself can drop (its conn\r\n    // detach no longer owns the controller slot).\r\n    assert!(\r\n        broker.test_seed_wedged_local_controller(sid, 1_000),\r\n        \"precondition: the wedge seed must install on the hosted session\"\r\n    );\r\n    drop(a);\r\n\r\n    // Broker truth reads WEDGED immediately — the seed is already past-deadline\r\n    // (no maturation sleep, no race against the round-4 watchdog).\r\n    assert!(\r\n        broker.any_local_controller_wedged(),\r\n        \"precondition: the old-gen local controller conn must read WEDGED before the trial\"\r\n    );\r\n\r\n    // ── Drive the REAL promotion path (supervise_brain → run_trial) with a scripted\r\n    //    AppliedPending trial whose candidate signals ready but whose DRAINED gate reads\r\n    //    the live wedged broker. ──\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let env = Arc::new(FalsePromoteEnv {\r\n        broker: Arc::clone(&broker),\r\n        ready: Mutex::new(None),\r\n        promotions: Mutex::new(Vec::new()),\r\n        rollbacks: Mutex::new(Vec::new()),\r\n        stop: Arc::clone(&stop),\r\n    });\r\n    let env_sup = Arc::clone(&env);\r\n    let stop_sup = Arc::clone(&stop);\r\n    let stop_spawn = Arc::clone(&stop);\r\n    let ready_spawn = Arc::clone(&env);\r\n    let sup = 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(5),\r\n            env_sup.as_ref(),\r\n            Duration::from_secs(20), // generous window — the take + drain arrive well inside it\r\n            move |gen, _reason, binary| {\r\n                if binary.is_some() {\r\n                    // A rollback spawn would mean the trial FAILED to promote — end the\r\n                    // run so the test surfaces the empty `promotions` rather than hanging.\r\n                    stop_spawn.store(true, Ordering::Relaxed);\r\n                    return candidate_child();\r\n                }\r\n                // The candidate signals ready for its generation, then stays alive.\r\n                *ready_spawn.ready.lock().unwrap() = Some(gen);\r\n                candidate_child()\r\n            },\r\n        );\r\n    });\r\n\r\n    // Several ticks past the shrunk deadline: ready is stamped, but the old-gen conn is\r\n    // still wedged → the trial MUST NOT have promoted (RED-first: without the drained\r\n    // gate it promotes here on ready alone — the 22:47 false-promote).\r\n    thread::sleep(Duration::from_millis(1200));\r\n    assert!(\r\n        env.promotions.lock().unwrap().is_empty(),\r\n        \"a ready candidate must NOT promote while the OLD generation's conn is still wedged\"\r\n    );\r\n    assert!(\r\n        env.rollbacks.lock().unwrap().is_empty(),\r\n        \"and it must not have rolled back yet — the window is far from elapsed\"\r\n    );\r\n\r\n    // ── Drain the old-gen conn: a second operator's take stall-evicts the wedged local\r\n    //    incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. ──\r\n    remote_take(&name, sid);\r\n\r\n    // Poll for the promotion the drain unblocks (bounded — the gate flips within a tick\r\n    // of the evict; a stall-evict is prompt, not a hang).\r\n    let deadline = Instant::now() + Duration::from_secs(8);\r\n    let promoted = loop {\r\n        if !env.promotions.lock().unwrap().is_empty() {\r\n            break true;\r\n        }\r\n…\n    };\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,null,348,null,375]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\false_promote.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":376,"totalBytes":14567,"outputLines":343,"outputBytes":14567,"shownRange":{"start":1,"end":343},"nextOffset":344}}},"isError":false,"timestamp":1784195796299}}
{"type":"message","id":"9710764b","parentId":"43b12063","timestamp":"2026-07-16T09:56:36.299Z","message":{"role":"toolResult","toolCallId":"call_MNNDN4XsjyfnCRvWfz2rs47C|fc_0cc7a0c9a797f232016a58aad43550819a932ba29614ccb037","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":1784195796299}}
{"type":"message","id":"d689dd23","parentId":"9710764b","timestamp":"2026-07-16T09:56:36.300Z","message":{"role":"toolResult","toolCallId":"call_8T09JSC1XIofMGXmo14ZmMxm|fc_0cc7a0c9a797f232016a58aad43520819a81477da78686c988","toolName":"grep","content":[{"type":"text","text":"# CHANGELOG.md\n 194|\n*195|A reliability patch closing the last update-wedge failure mode: a stuck session\n 196|consumer can no longer freeze the daemon under load.\n 197|\n 198|> **Applying this update needs a daemon restart.** Unlike the recent seamless\n\n# docs/\n## KNOWN-HAZARDS.md\n 623|\n*624|### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n*625|<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n 626|- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a contro...\n*627|- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n 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 637|\n*638|### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n*639|<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n*640|- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...\n 641|- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On dead...\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 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\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs/KNOWN-HAZARDS.md, docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md, CHANGELOG.md, CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":12,"fileCount":3,"files":["docs/KNOWN-HAZARDS.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","CHANGELOG.md"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":8},{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","count":3},{"path":"CHANGELOG.md","count":1}],"truncated":true,"displayContent":"# CHANGELOG.md\n 194│\n*195│A reliability patch closing the last update-wedge failure mode: a stuck session\n 196│consumer can no longer freeze the daemon under load.\n 197│\n 198│> **Applying this update needs a daemon restart.** Unlike the recent seamless\n\n# docs/\n## KNOWN-HAZARDS.md\n 623│\n*624│### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n*625│<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n 626│- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a contro...\n*627│- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n 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 637│\n*638│### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n*639│<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n*640│- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...\n 641│- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On dead...\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 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","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195796300}}
{"type":"message","id":"83b6f92f","parentId":"d689dd23","timestamp":"2026-07-16T09:56:36.304Z","message":{"role":"toolResult","toolCallId":"call_kPWGuYDO30jMFCFJVNYBqzDK|fc_0cc7a0c9a797f232016a58aad4354c819ab0ba6a9096897478","toolName":"read","content":[{"type":"text","text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\n//! broker connection (UPDATE-WEDGE round-4).\n//!\n//! ## The failure class this kills\n//!\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n//! A connected consumer that stops reading (an `rc --take` whose process\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\n//! connection, so the writer (and every write queued behind the same gate)\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\n//! brain restart).\n//!\n//! ## The primitive\n//!\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n//! so an unbounded bypass write cannot compile. Every write:\n//!\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n//!    write);\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n//! 3. on success, returns the half to the gate.\n//!\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n//! completion APC makes the wedged `SleepEx` loop return with\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n//! the same abort (the stuck op it waited behind is the wedge).\n//!\n//! ## Poison / retire invariant (both OSes)\n//!\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n//! connection is poisoned, idempotently — a length-prefixed frame may have\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n//! (3) the buffer of the canceled op is released only after the op reports\n//! completion (`write_frame` returns only after the completion APC — the\n//! synchronous return IS the completion handshake); (4) the retired half is\n//! parked in a graveyard slot — never dropped while the watchdog might still\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n//! client off, so the old client's next access fails, a re-issued server read\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n//! cancel→disconnect window.\n//!\n//! No new output queue (the bounded per-sink queues + isolated writer threads\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n//! healthy connections; only the poison/retire path is new.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse interprocess::local_socket::SendHalf;\n\nuse crate::codec::write_frame;\nuse crate::frame::Envelope;\n\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\n/// record in one process shares one base; lazily initialized on first stamp\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n/// not an absolute one.\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\n\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n/// thing by both daemon entries, next to `scrub_identity_env`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn init_log_anchor() {\n    let _ = MONO_ANCHOR.set(Instant::now());\n}\n\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n/// stderr has no per-line timestamps and broker+brain share one file — without\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n/// to field incident times (wall).\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn log_stamp() -> String {\n    let wall_ms = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .map(|d| d.as_millis())\n        .unwrap_or(0);\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\n}\n\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n/// conn can be controller + viewer + presence subscriber over its life), and\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\nconst LABEL_CAP: usize = 240;\n\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n#[derive(Debug, PartialEq, Eq)]\npub(crate) enum ConnWrite {\n    /// The frame was fully written.\n    Done,\n    /// The pre-write gate closure declined (e.g. a superseded controller\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\n    Superseded,\n}\n\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\nstruct Gate {\n    half: Option<SendHalf>,\n}\n\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\nstruct Dog {\n    /// `Some((op_id, deadline))` while a write is in flight.\n    inflight: Option<(u64, Instant)>,\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    shutdown: bool,\n}\n\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n/// the watchdog must not keep the conn alive).\nstruct Inner {\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n    id: u64,\n    /// Accreted attribution facts (subscriber role, endpoint/session where\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    label: Mutex<String>,\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n    first_write_logged: AtomicBool,\n    gate: Mutex<Gate>,\n    gate_cv: Condvar,\n    dog: Mutex<Dog>,\n    dog_cv: Condvar,\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n    raw: i32,\n}\n\n/// Recover a mutex guard even if a holder panicked — the state these guards\n/// protect is a plain slot/flag pair, valid on every panic boundary.\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n    match m.lock() {\n        Ok(g) => g,\n        Err(p) => p.into_inner(),\n    }\n}\n\n#[cfg(windows)]\nfn raw_of(half: &SendHalf) -> isize {\n    use std::os::windows::io::{AsHandle, AsRawHandle};\n    match half {\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\n    }\n}\n\n#[cfg(unix)]\nfn raw_of(half: &SendHalf) -> i32 {\n    use std::os::fd::{AsFd, AsRawFd};\n    match half {\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\n    }\n}\n\n#[cfg(windows)]\nmod win {\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\n    // (the deelevate.rs / daemon.rs posture).\n    extern \"system\" {\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\n    }\n}\n\nimpl Inner {\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn attribution(&self) -> String {\n        let label = recover(&self.label);\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n    }\n\n    /// Render the once-per-conn retirement record — the F-039 token split\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n    /// already-dead counterpart, the offline-peer dial churn) emits the\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n    /// SAME attribution fields — quieter class, never thinner (the retired\n    /// class is what discriminates fresh-carrier churn from interleave\n    /// artifact in the field). Behavior is identical either way: poison,\n    /// abort, retire, never reuse.\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n        if timed_out {\n            format!(\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n                self.attribution(),\n                self.bound.as_millis()\n            )\n        } else {\n            let cause = match cause {\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\n                None => \"write failed\".to_string(),\n            };\n            format!(\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n                 broker conn poisoned + retired, never reused (same retire behavior \\\n                 as POISONED — only the failure class differs) \\\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n                self.attribution()\n            )\n        }\n    }\n\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n    /// timeout/cancel leg is the retirement record itself.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n            eprintln!(\n                \"{}\",\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n            );\n        }\n        #[cfg(windows)]\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\n        unsafe {\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n        #[cfg(unix)]\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\n        // every future op — on Unix this one call IS the whole teardown.\n        unsafe {\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\n        }\n        // Wake gate-waiters so they observe the poison instead of sleeping\n        // out their full deadline, and the watchdog so it re-evaluates.\n        self.gate_cv.notify_all();\n        self.dog_cv.notify_all();\n    }\n\n    /// Sever the physical connection (exactly once), AFTER the canceled op\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n    /// client off (its next access fails), makes a re-issued server-side\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n    // On Unix the cfg(windows) teardown strips out and the idempotence\n    // guard's early `return` becomes the function tail — needless_return\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\n    #[cfg_attr(unix, allow(clippy::needless_return))]\n    fn sever(&self) {\n        if self.severed.swap(true, Ordering::AcqRel) {\n            return;\n        }\n        #[cfg(windows)]\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n        unsafe {\n            let _ = win::DisconnectNamedPipe(self.raw);\n…\n        }\n    }\n…\n}\n\n[Showing lines 1-333 of 815. Use :334 to continue]"}],"details":{"truncation":{"content":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\r\n//! broker connection (UPDATE-WEDGE round-4).\r\n//!\r\n//! ## The failure class this kills\r\n//!\r\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\r\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\r\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\r\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\r\n//! A connected consumer that stops reading (an `rc --take` whose process\r\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\r\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\r\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\r\n//! connection, so the writer (and every write queued behind the same gate)\r\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\r\n//! brain restart).\r\n//!\r\n//! ## The primitive\r\n//!\r\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\r\n//! so an unbounded bypass write cannot compile. Every write:\r\n//!\r\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\r\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\r\n//!    write);\r\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\r\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\r\n//! 3. on success, returns the half to the gate.\r\n//!\r\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\r\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\r\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\r\n//! completion APC makes the wedged `SleepEx` loop return with\r\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\r\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\r\n//! the same abort (the stuck op it waited behind is the wedge).\r\n//!\r\n//! ## Poison / retire invariant (both OSes)\r\n//!\r\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\r\n//! connection is poisoned, idempotently — a length-prefixed frame may have\r\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\r\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\r\n//! (3) the buffer of the canceled op is released only after the op reports\r\n//! completion (`write_frame` returns only after the completion APC — the\r\n//! synchronous return IS the completion handshake); (4) the retired half is\r\n//! parked in a graveyard slot — never dropped while the watchdog might still\r\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\r\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\r\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\r\n//! client off, so the old client's next access fails, a re-issued server read\r\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\r\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\r\n//! cancel→disconnect window.\r\n//!\r\n//! No new output queue (the bounded per-sink queues + isolated writer threads\r\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\r\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\r\n//! healthy connections; only the poison/retire path is new.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n\r\nuse std::io;\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\r\nuse std::thread::{self, JoinHandle};\r\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\r\n\r\nuse interprocess::local_socket::SendHalf;\r\n\r\nuse crate::codec::write_frame;\r\nuse crate::frame::Envelope;\r\n\r\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\r\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\r\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\r\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\r\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\r\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\r\n/// record in one process shares one base; lazily initialized on first stamp\r\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\r\n/// not an absolute one.\r\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\r\n\r\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\r\n/// thing by both daemon entries, next to `scrub_identity_env`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn init_log_anchor() {\r\n    let _ = MONO_ANCHOR.set(Instant::now());\r\n}\r\n\r\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\r\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\r\n/// stderr has no per-line timestamps and broker+brain share one file — without\r\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\r\n/// to field incident times (wall).\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn log_stamp() -> String {\r\n    let wall_ms = SystemTime::now()\r\n        .duration_since(UNIX_EPOCH)\r\n        .map(|d| d.as_millis())\r\n        .unwrap_or(0);\r\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\r\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\r\n}\r\n\r\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\r\n/// conn can be controller + viewer + presence subscriber over its life), and\r\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\r\nconst LABEL_CAP: usize = 240;\r\n\r\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\r\n#[derive(Debug, PartialEq, Eq)]\r\npub(crate) enum ConnWrite {\r\n    /// The frame was fully written.\r\n    Done,\r\n    /// The pre-write gate closure declined (e.g. a superseded controller\r\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\r\n    Superseded,\r\n}\r\n\r\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\r\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\r\nstruct Gate {\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\r\nstruct Dog {\r\n    /// `Some((op_id, deadline))` while a write is in flight.\r\n    inflight: Option<(u64, Instant)>,\r\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\r\n    shutdown: bool,\r\n}\r\n\r\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\r\n/// the watchdog must not keep the conn alive).\r\nstruct Inner {\r\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\r\n    id: u64,\r\n    /// Accreted attribution facts (subscriber role, endpoint/session where\r\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\r\n    label: Mutex<String>,\r\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\r\n    first_write_logged: AtomicBool,\r\n    gate: Mutex<Gate>,\r\n    gate_cv: Condvar,\r\n    dog: Mutex<Dog>,\r\n    dog_cv: Condvar,\r\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\r\n    /// conn refuses every future write and is never re-gated.\r\n    poisoned: AtomicBool,\r\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\r\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\r\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\r\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\r\n    /// an ordinary disconnect.\r\n    timed_out: AtomicBool,\r\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\r\n    severed: AtomicBool,\r\n    /// A retired (poisoned) half parks here so its OS handle stays alive\r\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\r\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\r\n    graveyard: Mutex<Option<SendHalf>>,\r\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\r\n    op_seq: AtomicU64,\r\n    /// The absolute per-write bound (gate-wait + OS write completion).\r\n    bound: Duration,\r\n    /// The raw OS handle/fd of the underlying stream, captured at\r\n    /// construction for the out-of-band abort. The recv and send halves share\r\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\r\n    /// aborting this handle covers a parked read too. Valid for the whole\r\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\r\n    /// `graveyard` until drop.\r\n    #[cfg(windows)]\r\n    raw: isize,\r\n    #[cfg(unix)]\r\n    raw: i32,\r\n}\r\n\r\n/// Recover a mutex guard even if a holder panicked — the state these guards\r\n/// protect is a plain slot/flag pair, valid on every panic boundary.\r\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\r\n    match m.lock() {\r\n        Ok(g) => g,\r\n        Err(p) => p.into_inner(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nfn raw_of(half: &SendHalf) -> isize {\r\n    use std::os::windows::io::{AsHandle, AsRawHandle};\r\n    match half {\r\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\r\n    }\r\n}\r\n\r\n#[cfg(unix)]\r\nfn raw_of(half: &SendHalf) -> i32 {\r\n    use std::os::fd::{AsFd, AsRawFd};\r\n    match half {\r\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nmod win {\r\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\r\n    // (the deelevate.rs / daemon.rs posture).\r\n    extern \"system\" {\r\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\r\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\r\n    }\r\n}\r\n\r\nimpl Inner {\r\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\r\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn attribution(&self) -> String {\r\n        let label = recover(&self.label);\r\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\r\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\r\n    }\r\n\r\n    /// Render the once-per-conn retirement record — the F-039 token split\r\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\r\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\r\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\r\n    /// already-dead counterpart, the offline-peer dial churn) emits the\r\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\r\n    /// SAME attribution fields — quieter class, never thinner (the retired\r\n    /// class is what discriminates fresh-carrier churn from interleave\r\n    /// artifact in the field). Behavior is identical either way: poison,\r\n    /// abort, retire, never reuse.\r\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\r\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\r\n        if timed_out {\r\n            format!(\r\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\r\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\r\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\r\n                self.attribution(),\r\n                self.bound.as_millis()\r\n            )\r\n        } else {\r\n            let cause = match cause {\r\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\r\n                None => \"write failed\".to_string(),\r\n            };\r\n            format!(\r\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\r\n                 broker conn poisoned + retired, never reused (same retire behavior \\\r\n                 as POISONED — only the failure class differs) \\\r\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\r\n                self.attribution()\r\n            )\r\n        }\r\n    }\r\n\r\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\r\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\r\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\r\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\r\n    /// timeout/cancel leg is the retirement record itself.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn lifecycle(&self, event: &str, extra: &str) {\r\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\r\n        eprintln!(\r\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\r\n            self.attribution()\r\n        );\r\n    }\r\n\r\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\r\n    /// the halves share one OS object) and latch the poison. Idempotent and\r\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\r\n    /// no-op, and re-cancel closes the race where a write was issued a beat\r\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\r\n    /// `cause` (the organic caller's write error) rides the retirement record\r\n    /// when the retirement is NOT deadline-driven.\r\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\r\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\r\n            // First poison of this conn: loud, once. The token is class-split\r\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\r\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\r\n            eprintln!(\r\n                \"{}\",\r\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\r\n            );\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\r\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\r\n        unsafe {\r\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\r\n        }\r\n        #[cfg(unix)]\r\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\r\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\r\n        // every future op — on Unix this one call IS the whole teardown.\r\n        unsafe {\r\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\r\n        }\r\n        // Wake gate-waiters so they observe the poison instead of sleeping\r\n        // out their full deadline, and the watchdog so it re-evaluates.\r\n        self.gate_cv.notify_all();\r\n        self.dog_cv.notify_all();\r\n    }\r\n\r\n    /// Sever the physical connection (exactly once), AFTER the canceled op\r\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\r\n    /// client off (its next access fails), makes a re-issued server-side\r\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\r\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\r\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\r\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\r\n    // On Unix the cfg(windows) teardown strips out and the idempotence\r\n    // guard's early `return` becomes the function tail — needless_return\r\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\r\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\r\n    #[cfg_attr(unix, allow(clippy::needless_return))]\r\n    fn sever(&self) {\r\n        if self.severed.swap(true, Ordering::AcqRel) {\r\n            return;\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\r\n        unsafe {\r\n            let _ = win::DisconnectNamedPipe(self.raw);\r","truncated":true,"truncatedBy":"lines","totalLines":815,"totalBytes":16187,"outputLines":333,"outputBytes":16187,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\r\n//! broker connection (UPDATE-WEDGE round-4).\r\n//!\r\n//! ## The failure class this kills\r\n//!\r\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\r\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\r\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\r\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\r\n//! A connected consumer that stops reading (an `rc --take` whose process\r\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\r\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\r\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\r\n//! connection, so the writer (and every write queued behind the same gate)\r\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\r\n//! brain restart).\r\n//!\r\n//! ## The primitive\r\n//!\r\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\r\n//! so an unbounded bypass write cannot compile. Every write:\r\n//!\r\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\r\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\r\n//!    write);\r\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\r\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\r\n//! 3. on success, returns the half to the gate.\r\n//!\r\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\r\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\r\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\r\n//! completion APC makes the wedged `SleepEx` loop return with\r\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\r\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\r\n//! the same abort (the stuck op it waited behind is the wedge).\r\n//!\r\n//! ## Poison / retire invariant (both OSes)\r\n//!\r\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\r\n//! connection is poisoned, idempotently — a length-prefixed frame may have\r\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\r\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\r\n//! (3) the buffer of the canceled op is released only after the op reports\r\n//! completion (`write_frame` returns only after the completion APC — the\r\n//! synchronous return IS the completion handshake); (4) the retired half is\r\n//! parked in a graveyard slot — never dropped while the watchdog might still\r\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\r\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\r\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\r\n//! client off, so the old client's next access fails, a re-issued server read\r\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\r\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\r\n//! cancel→disconnect window.\r\n//!\r\n//! No new output queue (the bounded per-sink queues + isolated writer threads\r\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\r\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\r\n//! healthy connections; only the poison/retire path is new.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n\r\nuse std::io;\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\r\nuse std::thread::{self, JoinHandle};\r\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\r\n\r\nuse interprocess::local_socket::SendHalf;\r\n\r\nuse crate::codec::write_frame;\r\nuse crate::frame::Envelope;\r\n\r\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\r\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\r\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\r\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\r\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\r\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\r\n/// record in one process shares one base; lazily initialized on first stamp\r\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\r\n/// not an absolute one.\r\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\r\n\r\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\r\n/// thing by both daemon entries, next to `scrub_identity_env`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn init_log_anchor() {\r\n    let _ = MONO_ANCHOR.set(Instant::now());\r\n}\r\n\r\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\r\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\r\n/// stderr has no per-line timestamps and broker+brain share one file — without\r\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\r\n/// to field incident times (wall).\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn log_stamp() -> String {\r\n    let wall_ms = SystemTime::now()\r\n        .duration_since(UNIX_EPOCH)\r\n        .map(|d| d.as_millis())\r\n        .unwrap_or(0);\r\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\r\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\r\n}\r\n\r\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\r\n/// conn can be controller + viewer + presence subscriber over its life), and\r\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\r\nconst LABEL_CAP: usize = 240;\r\n\r\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\r\n#[derive(Debug, PartialEq, Eq)]\r\npub(crate) enum ConnWrite {\r\n    /// The frame was fully written.\r\n    Done,\r\n    /// The pre-write gate closure declined (e.g. a superseded controller\r\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\r\n    Superseded,\r\n}\r\n\r\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\r\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\r\nstruct Gate {\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\r\nstruct Dog {\r\n    /// `Some((op_id, deadline))` while a write is in flight.\r\n    inflight: Option<(u64, Instant)>,\r\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\r\n    shutdown: bool,\r\n}\r\n\r\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\r\n/// the watchdog must not keep the conn alive).\r\nstruct Inner {\r\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\r\n    id: u64,\r\n    /// Accreted attribution facts (subscriber role, endpoint/session where\r\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\r\n    label: Mutex<String>,\r\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\r\n    first_write_logged: AtomicBool,\r\n    gate: Mutex<Gate>,\r\n    gate_cv: Condvar,\r\n    dog: Mutex<Dog>,\r\n    dog_cv: Condvar,\r\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\r\n    /// conn refuses every future write and is never re-gated.\r\n    poisoned: AtomicBool,\r\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\r\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\r\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\r\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\r\n    /// an ordinary disconnect.\r\n    timed_out: AtomicBool,\r\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\r\n    severed: AtomicBool,\r\n    /// A retired (poisoned) half parks here so its OS handle stays alive\r\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\r\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\r\n    graveyard: Mutex<Option<SendHalf>>,\r\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\r\n    op_seq: AtomicU64,\r\n    /// The absolute per-write bound (gate-wait + OS write completion).\r\n    bound: Duration,\r\n    /// The raw OS handle/fd of the underlying stream, captured at\r\n    /// construction for the out-of-band abort. The recv and send halves share\r\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\r\n    /// aborting this handle covers a parked read too. Valid for the whole\r\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\r\n    /// `graveyard` until drop.\r\n    #[cfg(windows)]\r\n    raw: isize,\r\n    #[cfg(unix)]\r\n    raw: i32,\r\n}\r\n\r\n/// Recover a mutex guard even if a holder panicked — the state these guards\r\n/// protect is a plain slot/flag pair, valid on every panic boundary.\r\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\r\n    match m.lock() {\r\n        Ok(g) => g,\r\n        Err(p) => p.into_inner(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nfn raw_of(half: &SendHalf) -> isize {\r\n    use std::os::windows::io::{AsHandle, AsRawHandle};\r\n    match half {\r\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\r\n    }\r\n}\r\n\r\n#[cfg(unix)]\r\nfn raw_of(half: &SendHalf) -> i32 {\r\n    use std::os::fd::{AsFd, AsRawFd};\r\n    match half {\r\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nmod win {\r\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\r\n    // (the deelevate.rs / daemon.rs posture).\r\n    extern \"system\" {\r\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\r\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\r\n    }\r\n}\r\n\r\nimpl Inner {\r\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\r\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn attribution(&self) -> String {\r\n        let label = recover(&self.label);\r\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\r\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\r\n    }\r\n\r\n    /// Render the once-per-conn retirement record — the F-039 token split\r\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\r\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\r\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\r\n    /// already-dead counterpart, the offline-peer dial churn) emits the\r\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\r\n    /// SAME attribution fields — quieter class, never thinner (the retired\r\n    /// class is what discriminates fresh-carrier churn from interleave\r\n    /// artifact in the field). Behavior is identical either way: poison,\r\n    /// abort, retire, never reuse.\r\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\r\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\r\n        if timed_out {\r\n            format!(\r\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\r\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\r\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\r\n                self.attribution(),\r\n                self.bound.as_millis()\r\n            )\r\n        } else {\r\n            let cause = match cause {\r\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\r\n                None => \"write failed\".to_string(),\r\n            };\r\n            format!(\r\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\r\n                 broker conn poisoned + retired, never reused (same retire behavior \\\r\n                 as POISONED — only the failure class differs) \\\r\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\r\n                self.attribution()\r\n            )\r\n        }\r\n    }\r\n\r\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\r\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\r\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\r\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\r\n    /// timeout/cancel leg is the retirement record itself.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn lifecycle(&self, event: &str, extra: &str) {\r\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\r\n        eprintln!(\r\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\r\n            self.attribution()\r\n        );\r\n    }\r\n\r\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\r\n    /// the halves share one OS object) and latch the poison. Idempotent and\r\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\r\n    /// no-op, and re-cancel closes the race where a write was issued a beat\r\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\r\n    /// `cause` (the organic caller's write error) rides the retirement record\r\n    /// when the retirement is NOT deadline-driven.\r\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\r\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\r\n            // First poison of this conn: loud, once. The token is class-split\r\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\r\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\r\n            eprintln!(\r\n                \"{}\",\r\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\r\n            );\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\r\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\r\n        unsafe {\r\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\r\n        }\r\n        #[cfg(unix)]\r\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\r\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\r\n        // every future op — on Unix this one call IS the whole teardown.\r\n        unsafe {\r\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\r\n        }\r\n        // Wake gate-waiters so they observe the poison instead of sleeping\r\n        // out their full deadline, and the watchdog so it re-evaluates.\r\n        self.gate_cv.notify_all();\r\n        self.dog_cv.notify_all();\r\n    }\r\n\r\n    /// Sever the physical connection (exactly once), AFTER the canceled op\r\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\r\n    /// client off (its next access fails), makes a re-issued server-side\r\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\r\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\r\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\r\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\r\n    // On Unix the cfg(windows) teardown strips out and the idempotence\r\n    // guard's early `return` becomes the function tail — needless_return\r\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\r\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\r\n    #[cfg_attr(unix, allow(clippy::needless_return))]\r\n    fn sever(&self) {\r\n        if self.severed.swap(true, Ordering::AcqRel) {\r\n            return;\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\r\n        unsafe {\r\n            let _ = win::DisconnectNamedPipe(self.raw);\r\n…\n        }\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,null,335,336,null,410]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":815,"totalBytes":16187,"outputLines":333,"outputBytes":16187,"shownRange":{"start":1,"end":333},"nextOffset":334}}},"isError":false,"timestamp":1784195796304}}
{"type":"message","id":"a7605160","parentId":"83b6f92f","timestamp":"2026-07-16T09:56:36.316Z","message":{"role":"toolResult","toolCallId":"call_OsK1ILYh2s4Z0hlJd9KXbQBc|fc_0cc7a0c9a797f232016a58aad43548819a8a33a750f11da255","toolName":"read","content":[{"type":"text","text":"    pub fn session_id(&self) -> Option<u64> {\n…\n    }\n\n    /// Snapshot durable state for a future handoff. **Test-only (restoration\n    /// D4-3)** — the production path reconstructs from the broker, never a\n    /// snapshot frame; see [`BrainState`].\n    pub fn snapshot(&self) -> BrainState {\n        BrainState {\n            session_id: self.session_id.expect(\"snapshot before a session exists\"),\n            generation: self.generation,\n            next_seq: self.next_seq,\n            gen_start_ms: self.gen_start_ms,\n        }\n    }\n\n    /// Spawn a session under a broker PTY; the broker auto-subscribes this\n    /// connection. Reads until the `spawned` reply (consuming any output that\n    /// races ahead).\n    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\n        self.spawn_session_pid(req)\n            .map(|(session_id, _)| session_id)\n    }\n\n    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\n    /// OS pid (when the backend exposes one) — measurement/observability\n    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\n    ///\n    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\n    /// calls [`Brain::read_event`] on every interleaved frame, so on a *resume-mode*\n    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\n    /// **different** session read here is cursor-processed (its cursor snaps; the\n    /// broker already counted it delivered) and then **discarded** by `_ => continue`\n    /// — that chunk is lost to its consumer and the broker will not re-send it.\n    /// Unreachable today (the supervised brain spawns nothing; a legacy single-session\n    /// seat has no other session); the daemon-hosted live-agent event pump must make\n    /// this non-blocking (one demux owns `read_event`, routing each frame to its\n    /// session) or route the frames it reads for other sessions, not drop them.\n    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\n        self.send(\n            KIND_SPAWN,\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Spawned { session_id, pid } => {\n                    self.session_id = Some(session_id);\n                    // A resume-mode brain tracks this fresh session per-session via\n                    // `read_event`'s first-sight `or_insert` seed (D4-2b) — no seed\n                    // needed here; a legacy brain (empty map) uses `next_seq`.\n                    return Ok((session_id, pid));\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => continue,\n            }\n        }\n    }\n\n    /// Inject input bytes into the current session (send-keys), the **legacy\n    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\n    /// of an effect that originates from a durable source (a spool row, a\n    /// sequenced injection), use [`Brain::send_effect`].\n    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\n        let sid = self.require_session()?;\n        self.send(\n            KIND_INPUT,\n            serde_json::to_value(InputReq {\n                session_id: sid,\n                data_b64: encode_bytes(bytes),\n                op_id: None,\n                minter: None,\n                ack: true,\n            })\n            .expect(\"InputReq serializes\"),\n        )\n    }\n\n    /// Inject input **idempotently**: the write carries the durable `op_id` so a\n    /// replay after a brain crash is deduped at the broker (exactly-once, B5).\n    /// `op_id` must come from a durable source and be reused verbatim on replay —\n    /// the same logical effect must always carry the same id, so the broker's\n    /// applied-set recognizes the re-drive. The broker answers with a\n    /// [`BrokerEvent::Applied`] for `(session, op_id)`.\n    ///\n    /// ACKED (`ack=true`): the caller waits on the [`BrokerEvent::Applied`] one at a\n    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward\n    /// operator/rc path — which never reads the ack and can send a FLOOD on one\n    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\n    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn send_effect(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\n        self.send_effect_inner(op, bytes, true)\n    }\n\n    /// Inject input idempotently like [`Brain::send_effect`] but request NO\n    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\n    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\n    /// discarded by `serve_attach` — and it can flood N input frames on ONE\n    /// brain↔broker conn without returning to read. With no ack the broker's\n    /// per-conn handler never writes back while servicing that flood, so the return\n    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the\n    /// broker still dedups by `(session, op_id)` at its applied-set.\n    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n    pub fn send_effect_no_ack(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\n        self.send_effect_inner(op, bytes, false)\n    }\n\n    /// Shared body for [`Brain::send_effect`] / [`Brain::send_effect_no_ack`]: the\n    /// only difference is whether the broker replies a `KIND_APPLIED` ack. The\n    /// [`MintedOp`] carries both the durable seq and its minter tag (ADR-0034), so\n    /// the wire's `op_id`/`minter` fields are stamped together — an unstamped op is\n    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\n    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\n        let sid = self.require_session()?;\n        self.send(\n            KIND_INPUT,\n            serde_json::to_value(InputReq {\n                session_id: sid,\n                data_b64: encode_bytes(bytes),\n                op_id: Some(op.seq),\n                minter: Some(op.minter),\n                ack,\n            })\n            .expect(\"InputReq serializes\"),\n        )\n    }\n\n    /// Resize the current session's PTY.\n    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\n        let sid = self.require_session()?;\n        self.send(\n            KIND_RESIZE,\n            serde_json::to_value(ResizeReq {\n                session_id: sid,\n                rows,\n                cols,\n            })\n            .expect(\"ResizeReq serializes\"),\n        )\n    }\n\n    /// Terminate the current session's child.\n    pub fn kill_session(&mut self) -> io::Result<()> {\n        let sid = self.require_session()?;\n        self.send(\n            KIND_KILL,\n            serde_json::to_value(KillReq { session_id: sid }).expect(\"KillReq serializes\"),\n        )\n    }\n\n    /// Read output events until the accumulated bytes contain `needle`,\n    /// appending all received output to `buf`. Returns the highest seq seen.\n    /// A session `Exit` before the needle is found is an error (the child died).\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n        loop {\n            if contains(buf, needle) {\n                return Ok(self.next_seq.saturating_sub(1));\n            }\n            match self.read_event()? {\n                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),\n…\n            }\n        }\n    }\n\n[Showing lines 429-588 of 2178. Use :589 to continue]"}],"details":{"truncation":{"content":"    }\r\n\r\n    /// Snapshot durable state for a future handoff. **Test-only (restoration\r\n    /// D4-3)** — the production path reconstructs from the broker, never a\r\n    /// snapshot frame; see [`BrainState`].\r\n    pub fn snapshot(&self) -> BrainState {\r\n        BrainState {\r\n            session_id: self.session_id.expect(\"snapshot before a session exists\"),\r\n            generation: self.generation,\r\n            next_seq: self.next_seq,\r\n            gen_start_ms: self.gen_start_ms,\r\n        }\r\n    }\r\n\r\n    /// Spawn a session under a broker PTY; the broker auto-subscribes this\r\n    /// connection. Reads until the `spawned` reply (consuming any output that\r\n    /// races ahead).\r\n    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\r\n        self.spawn_session_pid(req)\r\n            .map(|(session_id, _)| session_id)\r\n    }\r\n\r\n    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\r\n    /// OS pid (when the backend exposes one) — measurement/observability\r\n    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\r\n    ///\r\n    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\r\n    /// calls [`Brain::read_event`] on every interleaved frame, so on a *resume-mode*\r\n    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\r\n    /// **different** session read here is cursor-processed (its cursor snaps; the\r\n    /// broker already counted it delivered) and then **discarded** by `_ => continue`\r\n    /// — that chunk is lost to its consumer and the broker will not re-send it.\r\n    /// Unreachable today (the supervised brain spawns nothing; a legacy single-session\r\n    /// seat has no other session); the daemon-hosted live-agent event pump must make\r\n    /// this non-blocking (one demux owns `read_event`, routing each frame to its\r\n    /// session) or route the frames it reads for other sessions, not drop them.\r\n    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\r\n        self.send(\r\n            KIND_SPAWN,\r\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\r\n        )?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::Spawned { session_id, pid } => {\r\n                    self.session_id = Some(session_id);\r\n                    // A resume-mode brain tracks this fresh session per-session via\r\n                    // `read_event`'s first-sight `or_insert` seed (D4-2b) — no seed\r\n                    // needed here; a legacy brain (empty map) uses `next_seq`.\r\n                    return Ok((session_id, pid));\r\n                }\r\n                BrokerEvent::Error { message } => {\r\n                    return Err(io::Error::other(message));\r\n                }\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Inject input bytes into the current session (send-keys), the **legacy\r\n    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\r\n    /// of an effect that originates from a durable source (a spool row, a\r\n    /// sequenced injection), use [`Brain::send_effect`].\r\n    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_INPUT,\r\n            serde_json::to_value(InputReq {\r\n                session_id: sid,\r\n                data_b64: encode_bytes(bytes),\r\n                op_id: None,\r\n                minter: None,\r\n                ack: true,\r\n            })\r\n            .expect(\"InputReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Inject input **idempotently**: the write carries the durable `op_id` so a\r\n    /// replay after a brain crash is deduped at the broker (exactly-once, B5).\r\n    /// `op_id` must come from a durable source and be reused verbatim on replay —\r\n    /// the same logical effect must always carry the same id, so the broker's\r\n    /// applied-set recognizes the re-drive. The broker answers with a\r\n    /// [`BrokerEvent::Applied`] for `(session, op_id)`.\r\n    ///\r\n    /// ACKED (`ack=true`): the caller waits on the [`BrokerEvent::Applied`] one at a\r\n    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward\r\n    /// operator/rc path — which never reads the ack and can send a FLOOD on one\r\n    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\r\n    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    pub fn send_effect(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\r\n        self.send_effect_inner(op, bytes, true)\r\n    }\r\n\r\n    /// Inject input idempotently like [`Brain::send_effect`] but request NO\r\n    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\r\n    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\r\n    /// discarded by `serve_attach` — and it can flood N input frames on ONE\r\n    /// brain↔broker conn without returning to read. With no ack the broker's\r\n    /// per-conn handler never writes back while servicing that flood, so the return\r\n    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the\r\n    /// broker still dedups by `(session, op_id)` at its applied-set.\r\n    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n    pub fn send_effect_no_ack(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\r\n        self.send_effect_inner(op, bytes, false)\r\n    }\r\n\r\n    /// Shared body for [`Brain::send_effect`] / [`Brain::send_effect_no_ack`]: the\r\n    /// only difference is whether the broker replies a `KIND_APPLIED` ack. The\r\n    /// [`MintedOp`] carries both the durable seq and its minter tag (ADR-0034), so\r\n    /// the wire's `op_id`/`minter` fields are stamped together — an unstamped op is\r\n    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\r\n    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_INPUT,\r\n            serde_json::to_value(InputReq {\r\n                session_id: sid,\r\n                data_b64: encode_bytes(bytes),\r\n                op_id: Some(op.seq),\r\n                minter: Some(op.minter),\r\n                ack,\r\n            })\r\n            .expect(\"InputReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Resize the current session's PTY.\r\n    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_RESIZE,\r\n            serde_json::to_value(ResizeReq {\r\n                session_id: sid,\r\n                rows,\r\n                cols,\r\n            })\r\n            .expect(\"ResizeReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Terminate the current session's child.\r\n    pub fn kill_session(&mut self) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_KILL,\r\n            serde_json::to_value(KillReq { session_id: sid }).expect(\"KillReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Read output events until the accumulated bytes contain `needle`,\r\n    /// appending all received output to `buf`. Returns the highest seq seen.\r\n    /// A session `Exit` before the needle is found is an error (the child died).\r\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\r\n        loop {\r\n            if contains(buf, needle) {\r\n                return Ok(self.next_seq.saturating_sub(1));\r\n            }\r\n            match self.read_event()? {\r\n                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),\r","truncated":true,"truncatedBy":"lines","totalLines":1750,"totalBytes":7727,"outputLines":160,"outputBytes":7727,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn session_id(&self) -> Option<u64> {\n…\n    }\r\n\r\n    /// Snapshot durable state for a future handoff. **Test-only (restoration\r\n    /// D4-3)** — the production path reconstructs from the broker, never a\r\n    /// snapshot frame; see [`BrainState`].\r\n    pub fn snapshot(&self) -> BrainState {\r\n        BrainState {\r\n            session_id: self.session_id.expect(\"snapshot before a session exists\"),\r\n            generation: self.generation,\r\n            next_seq: self.next_seq,\r\n            gen_start_ms: self.gen_start_ms,\r\n        }\r\n    }\r\n\r\n    /// Spawn a session under a broker PTY; the broker auto-subscribes this\r\n    /// connection. Reads until the `spawned` reply (consuming any output that\r\n    /// races ahead).\r\n    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\r\n        self.spawn_session_pid(req)\r\n            .map(|(session_id, _)| session_id)\r\n    }\r\n\r\n    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\r\n    /// OS pid (when the backend exposes one) — measurement/observability\r\n    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\r\n    ///\r\n    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\r\n    /// calls [`Brain::read_event`] on every interleaved frame, so on a *resume-mode*\r\n    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\r\n    /// **different** session read here is cursor-processed (its cursor snaps; the\r\n    /// broker already counted it delivered) and then **discarded** by `_ => continue`\r\n    /// — that chunk is lost to its consumer and the broker will not re-send it.\r\n    /// Unreachable today (the supervised brain spawns nothing; a legacy single-session\r\n    /// seat has no other session); the daemon-hosted live-agent event pump must make\r\n    /// this non-blocking (one demux owns `read_event`, routing each frame to its\r\n    /// session) or route the frames it reads for other sessions, not drop them.\r\n    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\r\n        self.send(\r\n            KIND_SPAWN,\r\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\r\n        )?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::Spawned { session_id, pid } => {\r\n                    self.session_id = Some(session_id);\r\n                    // A resume-mode brain tracks this fresh session per-session via\r\n                    // `read_event`'s first-sight `or_insert` seed (D4-2b) — no seed\r\n                    // needed here; a legacy brain (empty map) uses `next_seq`.\r\n                    return Ok((session_id, pid));\r\n                }\r\n                BrokerEvent::Error { message } => {\r\n                    return Err(io::Error::other(message));\r\n                }\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Inject input bytes into the current session (send-keys), the **legacy\r\n    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\r\n    /// of an effect that originates from a durable source (a spool row, a\r\n    /// sequenced injection), use [`Brain::send_effect`].\r\n    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_INPUT,\r\n            serde_json::to_value(InputReq {\r\n                session_id: sid,\r\n                data_b64: encode_bytes(bytes),\r\n                op_id: None,\r\n                minter: None,\r\n                ack: true,\r\n            })\r\n            .expect(\"InputReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Inject input **idempotently**: the write carries the durable `op_id` so a\r\n    /// replay after a brain crash is deduped at the broker (exactly-once, B5).\r\n    /// `op_id` must come from a durable source and be reused verbatim on replay —\r\n    /// the same logical effect must always carry the same id, so the broker's\r\n    /// applied-set recognizes the re-drive. The broker answers with a\r\n    /// [`BrokerEvent::Applied`] for `(session, op_id)`.\r\n    ///\r\n    /// ACKED (`ack=true`): the caller waits on the [`BrokerEvent::Applied`] one at a\r\n    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward\r\n    /// operator/rc path — which never reads the ack and can send a FLOOD on one\r\n    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\r\n    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    pub fn send_effect(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\r\n        self.send_effect_inner(op, bytes, true)\r\n    }\r\n\r\n    /// Inject input idempotently like [`Brain::send_effect`] but request NO\r\n    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\r\n    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\r\n    /// discarded by `serve_attach` — and it can flood N input frames on ONE\r\n    /// brain↔broker conn without returning to read. With no ack the broker's\r\n    /// per-conn handler never writes back while servicing that flood, so the return\r\n    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the\r\n    /// broker still dedups by `(session, op_id)` at its applied-set.\r\n    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n    pub fn send_effect_no_ack(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\r\n        self.send_effect_inner(op, bytes, false)\r\n    }\r\n\r\n    /// Shared body for [`Brain::send_effect`] / [`Brain::send_effect_no_ack`]: the\r\n    /// only difference is whether the broker replies a `KIND_APPLIED` ack. The\r\n    /// [`MintedOp`] carries both the durable seq and its minter tag (ADR-0034), so\r\n    /// the wire's `op_id`/`minter` fields are stamped together — an unstamped op is\r\n    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\r\n    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_INPUT,\r\n            serde_json::to_value(InputReq {\r\n                session_id: sid,\r\n                data_b64: encode_bytes(bytes),\r\n                op_id: Some(op.seq),\r\n                minter: Some(op.minter),\r\n                ack,\r\n            })\r\n            .expect(\"InputReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Resize the current session's PTY.\r\n    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_RESIZE,\r\n            serde_json::to_value(ResizeReq {\r\n                session_id: sid,\r\n                rows,\r\n                cols,\r\n            })\r\n            .expect(\"ResizeReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Terminate the current session's child.\r\n    pub fn kill_session(&mut self) -> io::Result<()> {\r\n        let sid = self.require_session()?;\r\n        self.send(\r\n            KIND_KILL,\r\n            serde_json::to_value(KillReq { session_id: sid }).expect(\"KillReq serializes\"),\r\n        )\r\n    }\r\n\r\n    /// Read output events until the accumulated bytes contain `needle`,\r\n    /// appending all received output to `buf`. Returns the highest seq seen.\r\n    /// A session `Exit` before the needle is found is an error (the child died).\r\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\r\n        loop {\r\n            if contains(buf, needle) {\r\n                return Ok(self.next_seq.saturating_sub(1));\r\n            }\r\n            match self.read_event()? {\r\n                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),\r\n…\n            }\n        }\n    }","startLine":427,"lineNumbers":[427,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,null,599,600,601]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2178,"totalBytes":7727,"outputLines":160,"outputBytes":7727,"shownRange":{"start":429,"end":588},"nextOffset":589}}},"isError":false,"timestamp":1784195796316}}
{"type":"message","id":"a1dcc99c","parentId":"a7605160","timestamp":"2026-07-16T09:56:48.725Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Requesting lines around 1030**"},{"type":"toolCall","id":"call_Kqk8oCohDmDHqs8ONK6i0yxk|fc_0cc7a0c9a797f232016a58aae0a968819a99834a586d12ce85","name":"read","arguments":{"i":"Reading resume split symbols","path":"crates/spt-daemon/src/brain.rs","selector":"1010-1095,1580-1665"}},{"type":"toolCall","id":"call_N2D6vJ61GmasVdgQFK8QYXRU|fc_0cc7a0c9a797f232016a58aae0a980819aaa51cca923baf07b","name":"read","arguments":{"i":"Reading hazard details","path":"docs/KNOWN-HAZARDS.md","selector":"610-645"}},{"type":"toolCall","id":"call_ztJpY7fUEnhZEfmzytuKs3yH|fc_0cc7a0c9a797f232016a58aae0a988819a8650baf2a3bdb183","name":"read","arguments":{"i":"Reading release wedge note","path":"CHANGELOG.md","selector":"185-235"}},{"type":"toolCall","id":"call_ghohbL7JO5g9ssVIhEft4px7|fc_0cc7a0c9a797f232016a58aae0a98c819aae5da6cb1d477352","name":"grep","arguments":{"i":"Finding finish restart service path","pattern":"cmd_update_apply_finish|stop_daemon|start_daemon|request_stop|plan_stop|ServiceStop|replay_startup_endpoints|resume_restart_orphaned_endpoints","path":"crates/spt/src/cli.rs;crates/spt-daemon/src/daemon.rs;crates/spt-daemon/src/service.rs;crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/autostart.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_mC8fmkVDnT9lp3cV6yUfGVSK|fc_0cc7a0c9a797f232016a58aae0a994819a919d2f689f4ed148","name":"grep","arguments":{"i":"Finding daemon CLI enum dispatch","pattern":"enum DaemonCmd|DaemonCmd::Refresh|cmd_daemon_refresh|Daemon \\{|Cmd::Daemon","path":"crates/spt/src/cli.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_APMR0aUO64QJSP9pso8BGTFM|fc_0cc7a0c9a797f232016a58aae0a998819a8ffb948b8590869e","name":"grep","arguments":{"i":"Finding update enum definitions","pattern":"enum UpdateCmd|Apply \\{|Fetch \\{|Adapters \\{","path":"crates/spt/src/cli.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_qdjaD1bGzZQYdKoErmZqs1Ev|fc_0cc7a0c9a797f232016a58aae0a99c819aa8086cff993f9b16","name":"grep","arguments":{"i":"Finding service manager implementation","pattern":"trait ServiceManager|struct Windows|struct Systemd|fn start\\(|fn stop\\(|plan_start|plan_stop|schtasks|systemctl","path":"crates/spt-daemon/src/service.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":34252,"output":476,"cacheRead":200192,"cacheWrite":0,"totalTokens":234920,"reasoningTokens":12,"cost":{"input":0.17126000000000002,"output":0.014280000000000001,"cacheRead":0.10009599999999999,"cacheWrite":0,"total":0.285636}},"stopReason":"toolUse","timestamp":1784195796362,"responseId":"resp_0cc7a0c9a797f232016a58aad4a228819a86042aab74ba62ef","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cc7a0c9a797f232016a58aad73924819a99c4f1d82c4b084a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrgELJbuAJkOPyTbb7UOXYittuM7TL663T10wK8D1pKUPoFAw9jiXiyf7OdHxP0hGmNxT2qjoJgO8L65A65oyKNY3J63ReCsCzv5lGObt5uxE-AYDf8B6ie0TLwJZMa_T5KPzLH-YYXzijQYovU9-0-9B4mdPLzLOlvV_Y9C3Ptx7lKcFhcCJioOVjevEUr0o4m0Wyrwta4Mng1r3m-lYV17wWoJuQsYjbiXJk6FhAjMdycA1m9IVwpOgVOqdGDEO8ixAbjBIGGRk-rQDCyhAwUYJvo0DUvnB_Fu9uij4HqGGbQzaqAeJjnrIsowiFfLYTbRqvCtZvKgF-FAP4cvXejOp4gXJOoCSYSrPLfGtIY2QakPi-xqUa-c3IiLAL9dEHH5IvXmAAXt9HjE6u0SRz6KVKb44Kfzz1uSlO8bn2ruOOkmdL4k4sMcjb8cdfLRY9_bXigq0tH9hqO6ODc0Ln5Mv0yKWeISkuq8i6w8GxXVCPn6nGMHhPT6zbtJtfYiPxXQo2OnAHgieumGyR7viABwAoIyKy5CM81truloW8PJ3xdfYaSDbUQBccr3FRAZXyGzBgro9phdpI7-8dQipL24KX-4tx6Mi41Pm-yW3Zf0UjZbAGu4gyYcK33GVCQBmv5BBzaPfgawQw0kxPBD79m2NLuaQykZBix8mBJUPJ27laLORUlM4MY_yateFMSXZ4Damf-G4DGM2Q0LgEA0duvMsqNyfuNSBy63glWT0eq2k1cKN_L5xBRKFhCNJoo_px5jeGvJoahcGeGvfuCvli6Rhelbk8GFM5ZN5DXPoqVSKDwXNZf76179m8OdFtYynfn_AjOyTo89W15nNpASLveGktChTtTpkhIEYCk9DBBzVxtVyYXGpfV9ph-sDO7ew2WgzVQLbE70e0TukvxVbEhMsI6r_7Sef5LjZJ6CRo7-OqdF25lW3iwcz6HI3_gIVVc","summary":[{"type":"summary_text","text":"**Requesting lines around 1030**"}]},{"id":"fc_0cc7a0c9a797f232016a58aae0a968819a99834a586d12ce85","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading resume split symbols\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1010-1095,1580-1665\"}","call_id":"call_Kqk8oCohDmDHqs8ONK6i0yxk","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aae0a980819aaa51cca923baf07b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hazard details\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"610-645\"}","call_id":"call_N2D6vJ61GmasVdgQFK8QYXRU","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aae0a988819a8650baf2a3bdb183","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading release wedge note\",\"path\":\"CHANGELOG.md\",\"selector\":\"185-235\"}","call_id":"call_ztJpY7fUEnhZEfmzytuKs3yH","name":"read"},{"id":"fc_0cc7a0c9a797f232016a58aae0a98c819aae5da6cb1d477352","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding finish restart service path\",\"pattern\":\"cmd_update_apply_finish|stop_daemon|start_daemon|request_stop|plan_stop|ServiceStop|replay_startup_endpoints|resume_restart_orphaned_endpoints\",\"path\":\"crates/spt/src/cli.rs;crates/spt-daemon/src/daemon.rs;crates/spt-daemon/src/service.rs;crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/autostart.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_ghohbL7JO5g9ssVIhEft4px7","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aae0a994819a919d2f689f4ed148","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding daemon CLI enum dispatch\",\"pattern\":\"enum DaemonCmd|DaemonCmd::Refresh|cmd_daemon_refresh|Daemon \\\\{|Cmd::Daemon\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_mC8fmkVDnT9lp3cV6yUfGVSK","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aae0a998819a8ffb948b8590869e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding update enum definitions\",\"pattern\":\"enum UpdateCmd|Apply \\\\{|Fetch \\\\{|Adapters \\\\{\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_APMR0aUO64QJSP9pso8BGTFM","name":"grep"},{"id":"fc_0cc7a0c9a797f232016a58aae0a99c819aa8086cff993f9b16","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding service manager implementation\",\"pattern\":\"trait ServiceManager|struct Windows|struct Systemd|fn start\\\\(|fn stop\\\\(|plan_start|plan_stop|schtasks|systemctl\",\"path\":\"crates/spt-daemon/src/service.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_qdjaD1bGzZQYdKoErmZqs1Ev","name":"grep"}]},"duration":12360.312399999239,"ttft":2840.2216999996454,"contextSnapshot":{"promptTokens":234444,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Kqk8oCohDmDHqs8ONK6i0yxk|fc_0cc7a0c9a797f232016a58aae0a968819a99834a586d12ce85","toolName":"read","startedAt":"2026-07-16T09:56:48.725Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading resume split symbols"},"id":"4ab3f424","parentId":"a1dcc99c","timestamp":"2026-07-16T09:56:48.725Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_N2D6vJ61GmasVdgQFK8QYXRU|fc_0cc7a0c9a797f232016a58aae0a980819aaa51cca923baf07b","toolName":"read","startedAt":"2026-07-16T09:56:48.725Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading hazard details"},"id":"c43030d5","parentId":"4ab3f424","timestamp":"2026-07-16T09:56:48.725Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ztJpY7fUEnhZEfmzytuKs3yH|fc_0cc7a0c9a797f232016a58aae0a988819a8650baf2a3bdb183","toolName":"read","startedAt":"2026-07-16T09:56:48.726Z","args":{"path":"CHANGELOG.md"},"intent":"Reading release wedge note"},"id":"403a72ef","parentId":"c43030d5","timestamp":"2026-07-16T09:56:48.726Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ghohbL7JO5g9ssVIhEft4px7|fc_0cc7a0c9a797f232016a58aae0a98c819aae5da6cb1d477352","toolName":"grep","startedAt":"2026-07-16T09:56:48.726Z","args":{"path":"crates/spt/src/cli.rs;crates/spt-daemon/src/daemon.rs;crates/spt-daemon/src/service.rs;crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/autostart.rs"},"intent":"Finding finish restart service path"},"id":"65e37654","parentId":"403a72ef","timestamp":"2026-07-16T09:56:48.726Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mC8fmkVDnT9lp3cV6yUfGVSK|fc_0cc7a0c9a797f232016a58aae0a994819a919d2f689f4ed148","toolName":"grep","startedAt":"2026-07-16T09:56:48.726Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Finding daemon CLI enum dispatch"},"id":"0f3d21fb","parentId":"65e37654","timestamp":"2026-07-16T09:56:48.726Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_APMR0aUO64QJSP9pso8BGTFM|fc_0cc7a0c9a797f232016a58aae0a998819a8ffb948b8590869e","toolName":"grep","startedAt":"2026-07-16T09:56:48.726Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Finding update enum definitions"},"id":"48ca12fc","parentId":"0f3d21fb","timestamp":"2026-07-16T09:56:48.726Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qdjaD1bGzZQYdKoErmZqs1Ev|fc_0cc7a0c9a797f232016a58aae0a99c819aa8086cff993f9b16","toolName":"grep","startedAt":"2026-07-16T09:56:48.726Z","args":{"path":"crates/spt-daemon/src/service.rs"},"intent":"Finding service manager implementation"},"id":"e8bd2573","parentId":"48ca12fc","timestamp":"2026-07-16T09:56:48.726Z"}
{"type":"message","id":"40e54f42","parentId":"e8bd2573","timestamp":"2026-07-16T09:56:48.732Z","message":{"role":"toolResult","toolCallId":"call_qdjaD1bGzZQYdKoErmZqs1Ev|fc_0cc7a0c9a797f232016a58aae0a99c819aa8086cff993f9b16","toolName":"grep","content":[{"type":"text","text":" 13|//! presence means a unit is registered. The control ops (`start`/`stop`/\n*14|//! `is-active`) necessarily shell out to `systemctl --user` anyway.\n 15|//!\n 16|//! **Windows = the at-logon scheduled task is boot-only**, NOT a start/stop-\n 17|//! controllable service (DAEMON-LIFECYCLE decision b): `detected()` is false,\n...\n 56|// [impl->REQ-DAEMON-8]\n*57|pub fn plan_start(running: bool, service_detected: bool) -> StartPlan {\n 58|    if running {\n 59|        StartPlan::AlreadyRunning\n 60|    } else if service_detected {\n...\n 70|// [impl->REQ-DAEMON-6]\n*71|pub fn plan_stop(running: bool, service_detected: bool, service_active: bool) -> StopPlan {\n 72|    if service_detected && service_active {\n 73|        StopPlan::ServiceStop\n 74|    } else if running {\n...\n 100|    fn is_active(&self) -> bool;\n*101|    fn start(&self) -> io::Result<()>;\n*102|    fn stop(&self) -> io::Result<()>;\n 103|    /// Short label for `daemon status` + conflict warnings (\"systemd --user\").\n 104|    fn label(&self) -> &'static str;\n 105|    /// A boot-autostart note for `daemon status`, when the platform has one\n...\n 138|/// home) is NOT the service's home, so the service must not be driven for it\n*139|/// (else a custom-home command would shell out to `systemctl --user start\n 140|/// spt-daemon` — which serves the wrong home, or fails in a bus-less context).\n 141|/// Detection is gated on this so service routing only applies on the home the\n 142|/// service actually manages.\n...\n 148|#[cfg(target_os = \"linux\")]\n*149|fn run_systemctl(args: &[&str]) -> io::Result<()> {\n*150|    let status = Command::new(\"systemctl\").args(args).status()?;\n 151|    if status.success() {\n 152|        Ok(())\n 153|    } else {\n 154|        Err(io::Error::other(format!(\n*155|            \"systemctl {} failed ({status})\",\n 156|            args.join(\" \")\n 157|        )))\n 158|    }\n...\n 161|#[cfg(target_os = \"linux\")]\n*162|pub struct SystemdUserService;\n 163|\n 164|#[cfg(target_os = \"linux\")]\n 165|impl DaemonService for SystemdUserService {\n...\n 169|    fn is_active(&self) -> bool {\n*170|        Command::new(\"systemctl\")\n 171|            .args([\"--user\", \"is-active\", \"--quiet\", \"spt-daemon.service\"])\n 172|            .status()\n 173|            .map(|s| s.success())\n...\n 175|    }\n*176|    fn start(&self) -> io::Result<()> {\n*177|        run_systemctl(&[\"--user\", \"start\", \"spt-daemon.service\"])\n 178|    }\n*179|    fn stop(&self) -> io::Result<()> {\n*180|        run_systemctl(&[\"--user\", \"stop\", \"spt-daemon.service\"])\n 181|    }\n 182|    fn label(&self) -> &'static str {\n 183|        \"systemd --user\"\n...\n 185|    fn boot_hint(&self) -> Option<String> {\n*186|        let enabled = Command::new(\"systemctl\")\n 187|            .args([\"--user\", \"is-enabled\", \"--quiet\", \"spt-daemon.service\"])\n 188|            .status()\n 189|            .map(|s| s.success())\n...\n 202|fn logon_task_present() -> bool {\n*203|    Command::new(\"schtasks\")\n 204|        .args([\"/Query\", \"/TN\", \"spt-core daemon\"])\n 205|        .stdout(std::process::Stdio::null())\n 206|        .stderr(std::process::Stdio::null())\n...\n 223|/// The at-logon task's registered action string (\"Task To Run\"), via\n*224|/// `schtasks /Query /V`, or `None` when the task is absent/unreadable (or the\n 225|/// label is localized past our parse — best-effort, the nag simply doesn't fire).\n 226|#[cfg(windows)]\n 227|fn logon_task_action() -> Option<String> {\n*228|    let out = Command::new(\"schtasks\")\n 229|        .args([\"/Query\", \"/TN\", \"spt-core daemon\", \"/V\", \"/FO\", \"LIST\"])\n 230|        .output()\n 231|        .ok()?;\n...\n 244|#[cfg(windows)]\n*245|pub struct WindowsLogonTask;\n 246|\n 247|#[cfg(windows)]\n 248|impl DaemonService for WindowsLogonTask {\n...\n 256|    }\n*257|    fn start(&self) -> io::Result<()> {\n 258|        Err(io::Error::other(\n 259|            \"no controllable service on Windows (the logon task is boot-only)\",\n 260|        ))\n 261|    }\n*262|    fn stop(&self) -> io::Result<()> {\n 263|        Err(io::Error::other(\n 264|            \"no controllable service on Windows (the logon task is boot-only)\",\n 265|        ))\n...\n 302|    }\n*303|    fn start(&self) -> io::Result<()> {\n 304|        Err(io::Error::other(\"no managed service on this platform\"))\n 305|    }\n*306|    fn stop(&self) -> io::Result<()> {\n 307|        Err(io::Error::other(\"no managed service on this platform\"))\n 308|    }\n 309|    fn label(&self) -> &'static str {\n...\n 366|    #[test]\n*367|    fn plan_start_prefers_service_then_spawn_idempotent() {\n*368|        assert_eq!(plan_start(true, true), StartPlan::AlreadyRunning);\n*369|        assert_eq!(plan_start(true, false), StartPlan::AlreadyRunning);\n*370|        assert_eq!(plan_start(false, true), StartPlan::ServiceStart);\n*371|        assert_eq!(plan_start(false, false), StartPlan::ManualSpawn);\n 372|    }\n 373|\n 374|    // [unit->REQ-DAEMON-6] the stop router truth table: a registered+active\n...\n 379|    #[test]\n*380|    fn plan_stop_routes_managed_then_ipc_then_noop() {\n*381|        assert_eq!(plan_stop(true, true, true), StopPlan::ServiceStop);\n*382|        assert_eq!(plan_stop(true, true, false), StopPlan::IpcStop);\n*383|        assert_eq!(plan_stop(true, false, false), StopPlan::IpcStop);\n*384|        assert_eq!(plan_stop(false, false, false), StopPlan::NotRunning);\n 385|        // detected+inactive, nothing running ⇒ nothing to stop.\n*386|        assert_eq!(plan_stop(false, true, false), StopPlan::NotRunning);\n 387|    }\n 388|\n 389|    // [unit->REQ-DAEMON-7] `run` refuses only when it WOULD VANISH: on Windows"}],"details":{"scopePath":"crates/spt-daemon/src/service.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\service.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":35,"fileCount":1,"files":["crates/spt-daemon/src/service.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/service.rs","count":35}],"truncated":false,"displayContent":"  13│//! presence means a unit is registered. The control ops (`start`/`stop`/\n *14│//! `is-active`) necessarily shell out to `systemctl --user` anyway.\n  15│//!\n  16│//! **Windows = the at-logon scheduled task is boot-only**, NOT a start/stop-\n  17│//! controllable service (DAEMON-LIFECYCLE decision b): `detected()` is false,\n    │...\n  56│// [impl->REQ-DAEMON-8]\n *57│pub fn plan_start(running: bool, service_detected: bool) -> StartPlan {\n  58│    if running {\n  59│        StartPlan::AlreadyRunning\n  60│    } else if service_detected {\n    │...\n  70│// [impl->REQ-DAEMON-6]\n *71│pub fn plan_stop(running: bool, service_detected: bool, service_active: bool) -> StopPlan {\n  72│    if service_detected && service_active {\n  73│        StopPlan::ServiceStop\n  74│    } else if running {\n    │...\n 100│    fn is_active(&self) -> bool;\n*101│    fn start(&self) -> io::Result<()>;\n*102│    fn stop(&self) -> io::Result<()>;\n 103│    /// Short label for `daemon status` + conflict warnings (\"systemd --user\").\n 104│    fn label(&self) -> &'static str;\n 105│    /// A boot-autostart note for `daemon status`, when the platform has one\n    │...\n 138│/// home) is NOT the service's home, so the service must not be driven for it\n*139│/// (else a custom-home command would shell out to `systemctl --user start\n 140│/// spt-daemon` — which serves the wrong home, or fails in a bus-less context).\n 141│/// Detection is gated on this so service routing only applies on the home the\n 142│/// service actually manages.\n    │...\n 148│#[cfg(target_os = \"linux\")]\n*149│fn run_systemctl(args: &[&str]) -> io::Result<()> {\n*150│    let status = Command::new(\"systemctl\").args(args).status()?;\n 151│    if status.success() {\n 152│        Ok(())\n 153│    } else {\n 154│        Err(io::Error::other(format!(\n*155│            \"systemctl {} failed ({status})\",\n 156│            args.join(\" \")\n 157│        )))\n 158│    }\n    │...\n 161│#[cfg(target_os = \"linux\")]\n*162│pub struct SystemdUserService;\n 163│\n 164│#[cfg(target_os = \"linux\")]\n 165│impl DaemonService for SystemdUserService {\n    │...\n 169│    fn is_active(&self) -> bool {\n*170│        Command::new(\"systemctl\")\n 171│            .args([\"--user\", \"is-active\", \"--quiet\", \"spt-daemon.service\"])\n 172│            .status()\n 173│            .map(|s| s.success())\n    │...\n 175│    }\n*176│    fn start(&self) -> io::Result<()> {\n*177│        run_systemctl(&[\"--user\", \"start\", \"spt-daemon.service\"])\n 178│    }\n*179│    fn stop(&self) -> io::Result<()> {\n*180│        run_systemctl(&[\"--user\", \"stop\", \"spt-daemon.service\"])\n 181│    }\n 182│    fn label(&self) -> &'static str {\n 183│        \"systemd --user\"\n    │...\n 185│    fn boot_hint(&self) -> Option<String> {\n*186│        let enabled = Command::new(\"systemctl\")\n 187│            .args([\"--user\", \"is-enabled\", \"--quiet\", \"spt-daemon.service\"])\n 188│            .status()\n 189│            .map(|s| s.success())\n    │...\n 202│fn logon_task_present() -> bool {\n*203│    Command::new(\"schtasks\")\n 204│        .args([\"/Query\", \"/TN\", \"spt-core daemon\"])\n 205│        .stdout(std::process::Stdio::null())\n 206│        .stderr(std::process::Stdio::null())\n    │...\n 223│/// The at-logon task's registered action string (\"Task To Run\"), via\n*224│/// `schtasks /Query /V`, or `None` when the task is absent/unreadable (or the\n 225│/// label is localized past our parse — best-effort, the nag simply doesn't fire).\n 226│#[cfg(windows)]\n 227│fn logon_task_action() -> Option<String> {\n*228│    let out = Command::new(\"schtasks\")\n 229│        .args([\"/Query\", \"/TN\", \"spt-core daemon\", \"/V\", \"/FO\", \"LIST\"])\n 230│        .output()\n 231│        .ok()?;\n    │...\n 244│#[cfg(windows)]\n*245│pub struct WindowsLogonTask;\n 246│\n 247│#[cfg(windows)]\n 248│impl DaemonService for WindowsLogonTask {\n    │...\n 256│    }\n*257│    fn start(&self) -> io::Result<()> {\n 258│        Err(io::Error::other(\n 259│            \"no controllable service on Windows (the logon task is boot-only)\",\n 260│        ))\n 261│    }\n*262│    fn stop(&self) -> io::Result<()> {\n 263│        Err(io::Error::other(\n 264│            \"no controllable service on Windows (the logon task is boot-only)\",\n 265│        ))\n    │...\n 302│    }\n*303│    fn start(&self) -> io::Result<()> {\n 304│        Err(io::Error::other(\"no managed service on this platform\"))\n 305│    }\n*306│    fn stop(&self) -> io::Result<()> {\n 307│        Err(io::Error::other(\"no managed service on this platform\"))\n 308│    }\n 309│    fn label(&self) -> &'static str {\n    │...\n 366│    #[test]\n*367│    fn plan_start_prefers_service_then_spawn_idempotent() {\n*368│        assert_eq!(plan_start(true, true), StartPlan::AlreadyRunning);\n*369│        assert_eq!(plan_start(true, false), StartPlan::AlreadyRunning);\n*370│        assert_eq!(plan_start(false, true), StartPlan::ServiceStart);\n*371│        assert_eq!(plan_start(false, false), StartPlan::ManualSpawn);\n 372│    }\n 373│\n 374│    // [unit->REQ-DAEMON-6] the stop router truth table: a registered+active\n    │...\n 379│    #[test]\n*380│    fn plan_stop_routes_managed_then_ipc_then_noop() {\n*381│        assert_eq!(plan_stop(true, true, true), StopPlan::ServiceStop);\n*382│        assert_eq!(plan_stop(true, true, false), StopPlan::IpcStop);\n*383│        assert_eq!(plan_stop(true, false, false), StopPlan::IpcStop);\n*384│        assert_eq!(plan_stop(false, false, false), StopPlan::NotRunning);\n 385│        // detected+inactive, nothing running ⇒ nothing to stop.\n*386│        assert_eq!(plan_stop(false, true, false), StopPlan::NotRunning);\n 387│    }\n 388│\n 389│    // [unit->REQ-DAEMON-7] `run` refuses only when it WOULD VANISH: on Windows"},"isError":false,"timestamp":1784195808732}}
{"type":"message","id":"753014e8","parentId":"40e54f42","timestamp":"2026-07-16T09:56:48.733Z","message":{"role":"toolResult","toolCallId":"call_APMR0aUO64QJSP9pso8BGTFM|fc_0cc7a0c9a797f232016a58aae0a998819a8ffb948b8590869e","toolName":"grep","content":[{"type":"text","text":" 989|#[derive(Subcommand)]\n*990|enum UpdateCmd {\n 991|    /// Apply the staged, verified self-update now.\n*992|    Apply {\n 993|        /// Finish onto the new version in one step: install it, then restart the\n 994|        /// daemon so both the coordinator and every live agent run the new\n 995|        /// version. Hosted sessions come back automatically — no manual restart.\n...\n 1002|    /// (then `spt update apply`). Bootstraps a node with no peer to pull from.\n*1003|    Fetch {\n 1004|        /// Accept a release on this channel instead of the node's pin (e.g.\n 1005|        /// `beta`). Default: the node's pinned channel.\n 1006|        #[arg(long)]\n...\n 1020|    /// the rest, and a summary line reports each outcome.\n*1021|    Adapters {\n 1022|        /// Adapters to update, comma-separated (e.g. `claude-spt,other`).\n 1023|        /// Omit to sweep every release-shipped adapter.\n 1024|        names: Option<String>,\n...\n 1443|        } => match action {\n*1444|            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n*1445|            Some(UpdateCmd::Fetch {\n 1446|                channel,\n 1447|                tag,\n 1448|                apply,\n...\n 1450|            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n*1451|            Some(UpdateCmd::Adapters { names }) => {\n 1452|                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n 1453|            }\n 1454|            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n...\n 12709|            parse(&[\"spt\", \"update\", \"fetch\", \"--apply\"]).unwrap().cmd,\n*12710|            Some(Cmd::Update { action: Some(UpdateCmd::Fetch { apply: true, .. }), .. })\n 12711|        ));\n 12712|        assert!(matches!(\n 12713|            parse(&[\"spt\", \"update\", \"fetch\"]).unwrap().cmd,\n*12714|            Some(Cmd::Update { action: Some(UpdateCmd::Fetch { apply: false, .. }), .. })\n 12715|        ));\n 12716|    }\n 12717|\n...\n 12815|            Some(Cmd::Update {\n*12816|                action: Some(UpdateCmd::Adapters { names: None }),\n 12817|                ..\n 12818|            })\n 12819|        ));\n...\n 12821|            Some(Cmd::Update {\n*12822|                action: Some(UpdateCmd::Adapters { names: Some(n) }),\n 12823|                ..\n 12824|            }) => assert_eq!(n, \"a,b\"),\n 12825|            _ => panic!(\"expected update adapters with a names list\"),\n...\n 14888|            Cmd::Update {\n*14889|                action: Some(UpdateCmd::Apply { finish: false }),\n 14890|                ..\n 14891|            }\n 14892|        ));\n...\n 14898|            Cmd::Update {\n*14899|                action: Some(UpdateCmd::Apply { finish: true }),\n 14900|                ..\n 14901|            }\n 14902|        ));"}],"details":{"scopePath":"crates/spt/src/cli.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":13,"fileCount":1,"files":["crates/spt/src/cli.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":13}],"truncated":false,"displayContent":"   989│#[derive(Subcommand)]\n  *990│enum UpdateCmd {\n   991│    /// Apply the staged, verified self-update now.\n  *992│    Apply {\n   993│        /// Finish onto the new version in one step: install it, then restart the\n   994│        /// daemon so both the coordinator and every live agent run the new\n   995│        /// version. Hosted sessions come back automatically — no manual restart.\n      │...\n  1002│    /// (then `spt update apply`). Bootstraps a node with no peer to pull from.\n *1003│    Fetch {\n  1004│        /// Accept a release on this channel instead of the node's pin (e.g.\n  1005│        /// `beta`). Default: the node's pinned channel.\n  1006│        #[arg(long)]\n      │...\n  1020│    /// the rest, and a summary line reports each outcome.\n *1021│    Adapters {\n  1022│        /// Adapters to update, comma-separated (e.g. `claude-spt,other`).\n  1023│        /// Omit to sweep every release-shipped adapter.\n  1024│        names: Option<String>,\n      │...\n  1443│        } => match action {\n *1444│            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n *1445│            Some(UpdateCmd::Fetch {\n  1446│                channel,\n  1447│                tag,\n  1448│                apply,\n      │...\n  1450│            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n *1451│            Some(UpdateCmd::Adapters { names }) => {\n  1452│                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n  1453│            }\n  1454│            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n      │...\n 12709│            parse(&[\"spt\", \"update\", \"fetch\", \"--apply\"]).unwrap().cmd,\n*12710│            Some(Cmd::Update { action: Some(UpdateCmd::Fetch { apply: true, .. }), .. })\n 12711│        ));\n 12712│        assert!(matches!(\n 12713│            parse(&[\"spt\", \"update\", \"fetch\"]).unwrap().cmd,\n*12714│            Some(Cmd::Update { action: Some(UpdateCmd::Fetch { apply: false, .. }), .. })\n 12715│        ));\n 12716│    }\n 12717│\n      │...\n 12815│            Some(Cmd::Update {\n*12816│                action: Some(UpdateCmd::Adapters { names: None }),\n 12817│                ..\n 12818│            })\n 12819│        ));\n      │...\n 12821│            Some(Cmd::Update {\n*12822│                action: Some(UpdateCmd::Adapters { names: Some(n) }),\n 12823│                ..\n 12824│            }) => assert_eq!(n, \"a,b\"),\n 12825│            _ => panic!(\"expected update adapters with a names list\"),\n      │...\n 14888│            Cmd::Update {\n*14889│                action: Some(UpdateCmd::Apply { finish: false }),\n 14890│                ..\n 14891│            }\n 14892│        ));\n      │...\n 14898│            Cmd::Update {\n*14899│                action: Some(UpdateCmd::Apply { finish: true }),\n 14900│                ..\n 14901│            }\n 14902│        ));"},"isError":false,"timestamp":1784195808733}}
{"type":"message","id":"5400151c","parentId":"753014e8","timestamp":"2026-07-16T09:56:48.734Z","message":{"role":"toolResult","toolCallId":"call_mC8fmkVDnT9lp3cV6yUfGVSK|fc_0cc7a0c9a797f232016a58aae0a994819a919d2f689f4ed148","toolName":"grep","content":[{"type":"text","text":" 291|    /// member subnets, local endpoints (M8 decision 25).\n*292|    Daemon {\n 293|        #[command(subcommand)]\n 294|        action: Option<DaemonCmd>,\n 295|    },\n...\n 555|#[derive(Subcommand)]\n*556|enum DaemonCmd {\n 557|    /// Run the per-machine daemon in the FOREGROUND — this process IS the\n 558|    /// daemon, blocking until signalled (the service unit's ExecStart, or\n 559|    /// manual debugging). Never detaches; for a background daemon use `start`.\n...\n 1470|        // [impl->REQ-CLI-2]\n*1471|        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {\n 1472|            DaemonCmd::Run { detached } => cmd_daemon_run(detached),\n 1473|            DaemonCmd::Start => cmd_daemon_start(),\n 1474|            DaemonCmd::Stop { force } => cmd_daemon_stop(force),\n...\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 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...\n 12932|            parse(&[\"spt\", \"daemon\", \"refresh\"]).unwrap().cmd,\n*12933|            Some(Cmd::Daemon {\n*12934|                action: Some(DaemonCmd::Refresh)\n 12935|            })\n 12936|        ));\n 12937|    }\n...\n 14728|            parse(&[\"spt\", \"daemon\"]).unwrap().cmd.unwrap(),\n*14729|            Cmd::Daemon { action: None }\n 14730|        ));\n 14731|        // A human-typed `daemon run` parses with `detached: false` → cmd_daemon_run\n 14732|        // keeps its foreground console; the hidden `--detached` (the auto-start\n...\n 14737|            parse(&[\"spt\", \"daemon\", \"run\"]).unwrap().cmd.unwrap(),\n*14738|            Cmd::Daemon {\n 14739|                action: Some(DaemonCmd::Run { detached: false })\n 14740|            }\n 14741|        ));\n...\n 14743|            parse(&[\"spt\", \"daemon\", \"run\", \"--detached\"]).unwrap().cmd.unwrap(),\n*14744|            Cmd::Daemon {\n 14745|                action: Some(DaemonCmd::Run { detached: true })\n 14746|            }\n 14747|        ));\n...\n 14750|            parse(&[\"spt\", \"daemon\", \"start\"]).unwrap().cmd.unwrap(),\n*14751|            Cmd::Daemon {\n 14752|                action: Some(DaemonCmd::Start)\n 14753|            }\n 14754|        ));\n...\n 14756|            parse(&[\"spt\", \"daemon\", \"stop\"]).unwrap().cmd.unwrap(),\n*14757|            Cmd::Daemon {\n 14758|                action: Some(DaemonCmd::Stop { force: false })\n 14759|            }\n 14760|        ));\n...\n 14762|            parse(&[\"spt\", \"daemon\", \"stop\", \"--force\"]).unwrap().cmd.unwrap(),\n*14763|            Cmd::Daemon {\n 14764|                action: Some(DaemonCmd::Stop { force: true })\n 14765|            }\n 14766|        ));\n...\n 14768|            parse(&[\"spt\", \"daemon\", \"status\"]).unwrap().cmd.unwrap(),\n*14769|            Cmd::Daemon {\n 14770|                action: Some(DaemonCmd::Status)\n 14771|            }\n 14772|        ));\n...\n 14779|            parse(&[\"spt\", \"daemon\", \"brain\"]).unwrap().cmd.unwrap(),\n*14780|            Cmd::Daemon {\n 14781|                action: Some(DaemonCmd::Brain { generation: 0, ref start_reason })\n 14782|            } if start_reason == \"cold\"\n 14783|        ));\n...\n 14787|                .cmd.unwrap(),\n*14788|            Cmd::Daemon {\n 14789|                action: Some(DaemonCmd::Brain { generation: 4, ref start_reason })\n 14790|            } if start_reason == \"update\"\n 14791|        ));"}],"details":{"scopePath":"crates/spt/src/cli.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":16,"fileCount":1,"files":["crates/spt/src/cli.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":16}],"truncated":false,"displayContent":"   291│    /// member subnets, local endpoints (M8 decision 25).\n  *292│    Daemon {\n   293│        #[command(subcommand)]\n   294│        action: Option<DaemonCmd>,\n   295│    },\n      │...\n   555│#[derive(Subcommand)]\n  *556│enum DaemonCmd {\n   557│    /// Run the per-machine daemon in the FOREGROUND — this process IS the\n   558│    /// daemon, blocking until signalled (the service unit's ExecStart, or\n   559│    /// manual debugging). Never detaches; for a background daemon use `start`.\n      │...\n  1470│        // [impl->REQ-CLI-2]\n *1471│        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {\n  1472│            DaemonCmd::Run { detached } => cmd_daemon_run(detached),\n  1473│            DaemonCmd::Start => cmd_daemon_start(),\n  1474│            DaemonCmd::Stop { force } => cmd_daemon_stop(force),\n      │...\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  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      │...\n 12932│            parse(&[\"spt\", \"daemon\", \"refresh\"]).unwrap().cmd,\n*12933│            Some(Cmd::Daemon {\n*12934│                action: Some(DaemonCmd::Refresh)\n 12935│            })\n 12936│        ));\n 12937│    }\n      │...\n 14728│            parse(&[\"spt\", \"daemon\"]).unwrap().cmd.unwrap(),\n*14729│            Cmd::Daemon { action: None }\n 14730│        ));\n 14731│        // A human-typed `daemon run` parses with `detached: false` → cmd_daemon_run\n 14732│        // keeps its foreground console; the hidden `--detached` (the auto-start\n      │...\n 14737│            parse(&[\"spt\", \"daemon\", \"run\"]).unwrap().cmd.unwrap(),\n*14738│            Cmd::Daemon {\n 14739│                action: Some(DaemonCmd::Run { detached: false })\n 14740│            }\n 14741│        ));\n      │...\n 14743│            parse(&[\"spt\", \"daemon\", \"run\", \"--detached\"]).unwrap().cmd.unwrap(),\n*14744│            Cmd::Daemon {\n 14745│                action: Some(DaemonCmd::Run { detached: true })\n 14746│            }\n 14747│        ));\n      │...\n 14750│            parse(&[\"spt\", \"daemon\", \"start\"]).unwrap().cmd.unwrap(),\n*14751│            Cmd::Daemon {\n 14752│                action: Some(DaemonCmd::Start)\n 14753│            }\n 14754│        ));\n      │...\n 14756│            parse(&[\"spt\", \"daemon\", \"stop\"]).unwrap().cmd.unwrap(),\n*14757│            Cmd::Daemon {\n 14758│                action: Some(DaemonCmd::Stop { force: false })\n 14759│            }\n 14760│        ));\n      │...\n 14762│            parse(&[\"spt\", \"daemon\", \"stop\", \"--force\"]).unwrap().cmd.unwrap(),\n*14763│            Cmd::Daemon {\n 14764│                action: Some(DaemonCmd::Stop { force: true })\n 14765│            }\n 14766│        ));\n      │...\n 14768│            parse(&[\"spt\", \"daemon\", \"status\"]).unwrap().cmd.unwrap(),\n*14769│            Cmd::Daemon {\n 14770│                action: Some(DaemonCmd::Status)\n 14771│            }\n 14772│        ));\n      │...\n 14779│            parse(&[\"spt\", \"daemon\", \"brain\"]).unwrap().cmd.unwrap(),\n*14780│            Cmd::Daemon {\n 14781│                action: Some(DaemonCmd::Brain { generation: 0, ref start_reason })\n 14782│            } if start_reason == \"cold\"\n 14783│        ));\n      │...\n 14787│                .cmd.unwrap(),\n*14788│            Cmd::Daemon {\n 14789│                action: Some(DaemonCmd::Brain { generation: 4, ref start_reason })\n 14790│            } if start_reason == \"update\"\n 14791│        ));"},"isError":false,"timestamp":1784195808734}}
{"type":"message","id":"e81ccc9e","parentId":"5400151c","timestamp":"2026-07-16T09:56:48.748Z","message":{"role":"toolResult","toolCallId":"call_Kqk8oCohDmDHqs8ONK6i0yxk|fc_0cc7a0c9a797f232016a58aae0a968819a99834a586d12ce85","toolName":"read","content":[{"type":"text","text":"    /// all session continuity by querying the persistent side, never a brain→brain\n    /// message. Returns the ids re-attached (empty when the broker hosts none —\n    /// the supervised daemon brain's no-op-today case).\n    ///\n    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,\n    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send\n    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is\n    /// at-least-once; input/effects stay exactly-once via the broker-owned\n    /// `EffectJournal`. The generation is **not** touched — this is a cold start\n    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.\n    ///\n    /// Each session is re-attached as a **VIEWER**, never a controller: a caller that\n    /// DRAINS the resumed output (`Brain::read_event`) gets output-continuity from its\n    /// cursor; a viewer never seizes/stall-evicts a controller slot (UPDATE-WEDGE-2,\n    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\n    /// set is empty today.\n    ///\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-2]\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n            // replay starts arriving, then re-attach (Viewer) from there.\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n    /// the observation cursor is all it needs; no attach also means no controller steal\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\n    /// request/reply conn.\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// The per-session resume cursor (the next seq still needed for a session\n    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.\n    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {\n        self.session_cursors.get(&session_id).copied()\n    }\n\n    /// The CONTROLLER's contiguous render cursor — the next seq still needed on\n    /// the legacy single-session (strict reject-gap) path. On a b4 drop-don't-block\n…\n    pub fn net_presence_subscribe_tail(&mut self) -> io::Result<()> {\n        self.presence_cursor = None; // baseline on the first live event (no floor)\n        self.send(\n            KIND_NET_PRESENCE_SUBSCRIBE,\n            serde_json::to_value(NetPresenceSubscribeReq { from_seq: u64::MAX })\n                .expect(\"NetPresenceSubscribeReq serializes\"),\n        )\n    }\n\n    /// Re-attach to a session's output from `from_seq` as a LOCAL CONTROLLER\n    /// (intent `Control`, identity `None`) — the legacy single-session serve\n    /// [`Brain::attach`] path.\n    ///\n    /// CAUTION — a `None`-identity Control attach does NOT \"never displace\":\n    /// `resolve_subscribe` (broker.rs) silently re-takes a FREE or SAME-LOCAL\n    /// (`None==None`) controller slot via `become_controller` (only a\n    /// DIFFERENT-REMOTE incumbent falls back to a silent viewer — the gate-#7\n    /// cross-operator guard). That silent re-take fires NO `Displaced` notice and\n    /// ORPHANS the prior local controller (the broker stops writing its output but\n    /// does not close the conn), FREEZING the session immediately; a >15s\n    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n    /// (only an actively-producing stolen session blocks long enough to evict).\n    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n    /// silently STEAL every local/free controller (incl. the operator's local\n    /// `spt rc`) it never drains, freezing the session\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL). [`Brain::resume_sessions`] re-attaches\n    /// as Viewer instead; Control is reserved for sessions the daemon brain drives.\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)\n    }\n\n    /// Release this connection's controller/viewer role on `session_id`\n    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\n    /// the viewport ends so the broker clears `driven_by` / `viewer_count`\n    /// promptly. Best-effort: a dropped connection clears the role anyway.\n    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {\n        self.send(\n            KIND_UNSUBSCRIBE,\n            serde_json::to_value(UnsubscribeReq { session_id })\n                .expect(\"UnsubscribeReq serializes\"),\n        )\n    }\n\n    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/\n    /// take, `by` carries the operator identity (`Some(node)` remote, `None`\n    /// local). The broker replies the resolved [`SubscribeOutcome`] as a\n    /// [`BrokerEvent::Subscribed`].\n    fn subscribe_with(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> io::Result<()> {\n        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain\n        // (seeded map) that subscribes from `from_seq` must RESET this session's\n        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then\n        // re-delivered (and the operator's render cursor dedups the overlap),\n        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,\n        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary\n        // BEFORE it is `attached` (the frame is dropped by the `if attached` gate\n        // but the snap-above cursor still advanced past K); `attach_as(sid, 0)`\n        // then re-subscribes from 0, and without this reset the broker's re-send of\n        // seq K arrives below the advanced cursor and is deduped → seq K never\n        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n        // legacy `next_seq` path; this only arms in resume mode.\n        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n        if !self.session_cursors.is_empty() {\n            self.session_cursors.insert(session_id, from_seq);\n        }\n        self.send(\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id,\n                from_seq,\n                intent,\n                by,\n            })\n            .expect(\"SubscribeReq serializes\"),\n        )\n    }\n"}],"details":{"displayContent":{"text":"    /// all session continuity by querying the persistent side, never a brain→brain\n    /// message. Returns the ids re-attached (empty when the broker hosts none —\n    /// the supervised daemon brain's no-op-today case).\n    ///\n    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,\n    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send\n    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is\n    /// at-least-once; input/effects stay exactly-once via the broker-owned\n    /// `EffectJournal`. The generation is **not** touched — this is a cold start\n    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.\n    ///\n    /// Each session is re-attached as a **VIEWER**, never a controller: a caller that\n    /// DRAINS the resumed output (`Brain::read_event`) gets output-continuity from its\n    /// cursor; a viewer never seizes/stall-evicts a controller slot (UPDATE-WEDGE-2,\n    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\n    /// set is empty today.\n    ///\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-2]\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n            // replay starts arriving, then re-attach (Viewer) from there.\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n    /// the observation cursor is all it needs; no attach also means no controller steal\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\n    /// request/reply conn.\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// The per-session resume cursor (the next seq still needed for a session\n    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.\n    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {\n        self.session_cursors.get(&session_id).copied()\n    }\n\n    /// The CONTROLLER's contiguous render cursor — the next seq still needed on\n    /// the legacy single-session (strict reject-gap) path. On a b4 drop-don't-block\n…\n    pub fn net_presence_subscribe_tail(&mut self) -> io::Result<()> {\n        self.presence_cursor = None; // baseline on the first live event (no floor)\n        self.send(\n            KIND_NET_PRESENCE_SUBSCRIBE,\n            serde_json::to_value(NetPresenceSubscribeReq { from_seq: u64::MAX })\n                .expect(\"NetPresenceSubscribeReq serializes\"),\n        )\n    }\n\n    /// Re-attach to a session's output from `from_seq` as a LOCAL CONTROLLER\n    /// (intent `Control`, identity `None`) — the legacy single-session serve\n    /// [`Brain::attach`] path.\n    ///\n    /// CAUTION — a `None`-identity Control attach does NOT \"never displace\":\n    /// `resolve_subscribe` (broker.rs) silently re-takes a FREE or SAME-LOCAL\n    /// (`None==None`) controller slot via `become_controller` (only a\n    /// DIFFERENT-REMOTE incumbent falls back to a silent viewer — the gate-#7\n    /// cross-operator guard). That silent re-take fires NO `Displaced` notice and\n    /// ORPHANS the prior local controller (the broker stops writing its output but\n    /// does not close the conn), FREEZING the session immediately; a >15s\n    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n    /// (only an actively-producing stolen session blocks long enough to evict).\n    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n    /// silently STEAL every local/free controller (incl. the operator's local\n    /// `spt rc`) it never drains, freezing the session\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL). [`Brain::resume_sessions`] re-attaches\n    /// as Viewer instead; Control is reserved for sessions the daemon brain drives.\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)\n    }\n\n    /// Release this connection's controller/viewer role on `session_id`\n    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\n    /// the viewport ends so the broker clears `driven_by` / `viewer_count`\n    /// promptly. Best-effort: a dropped connection clears the role anyway.\n    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {\n        self.send(\n            KIND_UNSUBSCRIBE,\n            serde_json::to_value(UnsubscribeReq { session_id })\n                .expect(\"UnsubscribeReq serializes\"),\n        )\n    }\n\n    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/\n    /// take, `by` carries the operator identity (`Some(node)` remote, `None`\n    /// local). The broker replies the resolved [`SubscribeOutcome`] as a\n    /// [`BrokerEvent::Subscribed`].\n    fn subscribe_with(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> io::Result<()> {\n        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain\n        // (seeded map) that subscribes from `from_seq` must RESET this session's\n        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then\n        // re-delivered (and the operator's render cursor dedups the overlap),\n        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,\n        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary\n        // BEFORE it is `attached` (the frame is dropped by the `if attached` gate\n        // but the snap-above cursor still advanced past K); `attach_as(sid, 0)`\n        // then re-subscribes from 0, and without this reset the broker's re-send of\n        // seq K arrives below the advanced cursor and is deduped → seq K never\n        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n        // legacy `next_seq` path; this only arms in resume mode.\n        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n        if !self.session_cursors.is_empty() {\n            self.session_cursors.insert(session_id, from_seq);\n        }\n        self.send(\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id,\n                from_seq,\n                intent,\n                by,\n            })\n            .expect(\"SubscribeReq serializes\"),\n        )\n    }\n","startLine":1010,"lineNumbers":[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,null,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784195808748}}
{"type":"message","id":"359b1159","parentId":"e81ccc9e","timestamp":"2026-07-16T09:56:48.753Z","message":{"role":"toolResult","toolCallId":"call_ghohbL7JO5g9ssVIhEft4px7|fc_0cc7a0c9a797f232016a58aae0a98c819aae5da6cb1d477352","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/\n### cli.rs\n 3961|fn cmd_daemon_start() -> i32 {\n*3962|    match spt_daemon::start_daemon() {\n 3963|        Ok(spt_daemon::StartOutcome::AlreadyRunning) => {\n 3964|            eprintln!(\"DAEMON_ALREADY_RUNNING\");\n 3965|            0\n...\n 4037|    }\n*4038|    match spt_daemon::stop_daemon() {\n 4039|        Ok(spt_daemon::StopOutcome::NotRunning) => {\n 4040|            eprintln!(\"DAEMON_NOT_RUNNING (already stopped)\");\n 4041|            0\n 4042|        }\n*4043|        Ok(spt_daemon::StopOutcome::ServiceStopped(label)) => {\n 4044|            eprintln!(\"DAEMON_STOPPED (via {label})\");\n 4045|            0\n 4046|        }\n...\n 4923|    if finish {\n*4924|        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n 4925|        if code == 0 {\n 4926|            land_staged_docs(&cache);\n 4927|        }\n...\n 5145|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n*5146|fn cmd_update_apply_finish(\n 5147|    cache: &spt_daemon::ReleaseCache,\n 5148|    keys: &std::path::Path,\n 5149|    exe: &std::path::Path,\n...\n 5184|        }\n*5185|        if let Err(e) = spt_daemon::stop_daemon() {\n 5186|            eprintln!(\n 5187|                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n 5188|            );\n...\n 5202|    }\n*5203|    match spt_daemon::start_daemon() {\n 5204|        Ok(_) => {\n 5205|            let (version, product_version, already) = match applied {\n 5206|                O::AppliedDaemonless {\n\n## spt-daemon/src/\n### daemon.rs\n 221|                    thread::spawn(|| {\n*222|                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n 223|                    });\n 224|                    // The node-local docs server (ADR-0036 §4): broker-side so\n 225|                    // docs are up whenever the daemon is. Loopback only; a bind\n...\n 472|// [impl->REQ-CLI-2]\n*473|pub fn request_stop() -> io::Result<()> {\n*474|    seedmap::request_stop(&seed_socket_name())\n 475|}\n 476|\n 477|/// Block (bounded) until a daemon answers the seed-control channel, or time\n*478|/// out. Shared by every start path (`ensure_running`, `start_daemon`):\n 479|/// race-tolerant — if two callers spawn at once, one wins the bind and this\n 480|/// passes either way.\n 481|fn wait_until_up() -> io::Result<()> {\n...\n 544|    /// Stopped through the named service manager.\n*545|    ServiceStopped(&'static str),\n 546|    /// Stopped a manual daemon over the seed-control IPC.\n 547|    IpcStopped,\n 548|}\n...\n 553|// [impl->REQ-DAEMON-6]\n*554|pub fn start_daemon() -> io::Result<StartOutcome> {\n 555|    let svc = crate::service::platform_service();\n 556|    match crate::service::plan_start(is_running(), svc.detected()) {\n 557|        crate::service::StartPlan::AlreadyRunning => Ok(StartOutcome::AlreadyRunning),\n...\n 584|// [impl->REQ-DAEMON-6]\n*585|pub fn stop_daemon() -> io::Result<StopOutcome> {\n 586|    let svc = crate::service::platform_service();\n*587|    match crate::service::plan_stop(is_running(), svc.detected(), svc.is_active()) {\n 588|        crate::service::StopPlan::NotRunning => Ok(StopOutcome::NotRunning),\n*589|        crate::service::StopPlan::ServiceStop => {\n 590|            svc.stop()?;\n*591|            Ok(StopOutcome::ServiceStopped(svc.label()))\n 592|        }\n 593|        crate::service::StopPlan::IpcStop => {\n*594|            request_stop()?;\n 595|            Ok(StopOutcome::IpcStopped)\n 596|        }\n 597|    }\n...\n 1720|            // self-sustaining pin doyle's cdb found feeding the stop-barrier convoy).\n*1721|            let _ = crate::seedmap::request_stop(&name);\n 1722|        });\n 1723|    }\n 1724|}\n### service.rs\n 47|    /// auto-restart-fight the teardown.\n*48|    ServiceStop,\n 49|    /// A manual daemon is running — graceful IPC stop.\n 50|    IpcStop,\n 51|}\n...\n 70|// [impl->REQ-DAEMON-6]\n*71|pub fn plan_stop(running: bool, service_detected: bool, service_active: bool) -> StopPlan {\n 72|    if service_detected && service_active {\n*73|        StopPlan::ServiceStop\n 74|    } else if running {\n 75|        StopPlan::IpcStop\n 76|    } else {\n...\n 379|    #[test]\n*380|    fn plan_stop_routes_managed_then_ipc_then_noop() {\n*381|        assert_eq!(plan_stop(true, true, true), StopPlan::ServiceStop);\n*382|        assert_eq!(plan_stop(true, true, false), StopPlan::IpcStop);\n*383|        assert_eq!(plan_stop(true, false, false), StopPlan::IpcStop);\n*384|        assert_eq!(plan_stop(false, false, false), StopPlan::NotRunning);\n 385|        // detected+inactive, nothing running ⇒ nothing to stop.\n*386|        assert_eq!(plan_stop(false, true, false), StopPlan::NotRunning);\n 387|    }\n 388|\n 389|    // [unit->REQ-DAEMON-7] `run` refuses only when it WOULD VANISH: on Windows\n### livehost.rs\n 417|/// action behind BOTH the wake-resume leg ([`resume_woken_endpoint`]) and the\n*418|/// daemon-restart survival pass ([`resume_restart_orphaned_endpoints`]). `reason_tag`\n 419|/// names the trigger in the log breadcrumb (`WAKE` / `DAEMON_RESTART`), scoped so an\n 420|/// operator can tell a woken resume from a restart-survival re-run. Records the custody\n 421|/// guard pid + clears any stale `host_error` on success; stamps `host_error` on\n...\n 549|// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n*550|pub fn resume_restart_orphaned_endpoints(\n 551|    owlery: &Path,\n 552|    registered: &[(AdapterRecord, Manifest)],\n 553|    adapters_dir: &Path,\n...\n 1002|            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n*1003|            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 1004|        }\n 1005|        while !stop.load(Ordering::SeqCst) {\n 1006|            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n### autostart.rs\n 36|// [impl->REQ-ENDPOINT-AUTOSTART]\n*37|pub fn replay_startup_endpoints(broker_name: &str) {\n 38|    let entries = DaemonConfig::load().startup_endpoints;\n 39|    if entries.is_empty() {\n 40|        return;\n...\n 126|\n*127|    /// Run `replay_startup_endpoints` on a watchdog thread and require it to\n 128|    /// RETURN within `budget` — a replay is best-effort and must never hang the\n 129|    /// daemon-boot thread. The socket name is intentionally bogus: these gates\n 130|    /// exercise only the pre-launch legs (empty list / loud-skip), which never\n...\n 134|        let handle = std::thread::spawn(move || {\n*135|            replay_startup_endpoints(\"bogus-broker-socket-never-dialed\");\n 136|            let _ = tx.send(());\n 137|        });\n 138|        rx.recv_timeout(budget).expect(\n*139|            \"replay_startup_endpoints must return promptly on the pre-launch paths \\\n 140|             (empty list / unresolved adapter loud-skip), never hang the boot thread\",\n 141|        );\n 142|        handle.join().expect(\"watchdog thread must not panic\");"}],"details":{"scopePath":"crates/spt/src/cli.rs, crates/spt-daemon/src/daemon.rs, crates/spt-daemon/src/service.rs, crates/spt-daemon/src/livehost.rs, crates/spt-daemon/src/autostart.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":35,"fileCount":5,"files":["crates/spt/src/cli.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/service.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/autostart.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":7},{"path":"crates/spt-daemon/src/daemon.rs","count":12},{"path":"crates/spt-daemon/src/service.rs","count":9},{"path":"crates/spt-daemon/src/livehost.rs","count":3},{"path":"crates/spt-daemon/src/autostart.rs","count":4}],"truncated":false,"displayContent":"# crates/\n\n## spt/src/\n### cli.rs\n 3961│fn cmd_daemon_start() -> i32 {\n*3962│    match spt_daemon::start_daemon() {\n 3963│        Ok(spt_daemon::StartOutcome::AlreadyRunning) => {\n 3964│            eprintln!(\"DAEMON_ALREADY_RUNNING\");\n 3965│            0\n     │...\n 4037│    }\n*4038│    match spt_daemon::stop_daemon() {\n 4039│        Ok(spt_daemon::StopOutcome::NotRunning) => {\n 4040│            eprintln!(\"DAEMON_NOT_RUNNING (already stopped)\");\n 4041│            0\n 4042│        }\n*4043│        Ok(spt_daemon::StopOutcome::ServiceStopped(label)) => {\n 4044│            eprintln!(\"DAEMON_STOPPED (via {label})\");\n 4045│            0\n 4046│        }\n     │...\n 4923│    if finish {\n*4924│        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n 4925│        if code == 0 {\n 4926│            land_staged_docs(&cache);\n 4927│        }\n     │...\n 5145│// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n*5146│fn cmd_update_apply_finish(\n 5147│    cache: &spt_daemon::ReleaseCache,\n 5148│    keys: &std::path::Path,\n 5149│    exe: &std::path::Path,\n     │...\n 5184│        }\n*5185│        if let Err(e) = spt_daemon::stop_daemon() {\n 5186│            eprintln!(\n 5187│                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n 5188│            );\n     │...\n 5202│    }\n*5203│    match spt_daemon::start_daemon() {\n 5204│        Ok(_) => {\n 5205│            let (version, product_version, already) = match applied {\n 5206│                O::AppliedDaemonless {\n\n## spt-daemon/src/\n### daemon.rs\n  221│                    thread::spawn(|| {\n *222│                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n  223│                    });\n  224│                    // The node-local docs server (ADR-0036 §4): broker-side so\n  225│                    // docs are up whenever the daemon is. Loopback only; a bind\n     │...\n  472│// [impl->REQ-CLI-2]\n *473│pub fn request_stop() -> io::Result<()> {\n *474│    seedmap::request_stop(&seed_socket_name())\n  475│}\n  476│\n  477│/// Block (bounded) until a daemon answers the seed-control channel, or time\n *478│/// out. Shared by every start path (`ensure_running`, `start_daemon`):\n  479│/// race-tolerant — if two callers spawn at once, one wins the bind and this\n  480│/// passes either way.\n  481│fn wait_until_up() -> io::Result<()> {\n     │...\n  544│    /// Stopped through the named service manager.\n *545│    ServiceStopped(&'static str),\n  546│    /// Stopped a manual daemon over the seed-control IPC.\n  547│    IpcStopped,\n  548│}\n     │...\n  553│// [impl->REQ-DAEMON-6]\n *554│pub fn start_daemon() -> io::Result<StartOutcome> {\n  555│    let svc = crate::service::platform_service();\n  556│    match crate::service::plan_start(is_running(), svc.detected()) {\n  557│        crate::service::StartPlan::AlreadyRunning => Ok(StartOutcome::AlreadyRunning),\n     │...\n  584│// [impl->REQ-DAEMON-6]\n *585│pub fn stop_daemon() -> io::Result<StopOutcome> {\n  586│    let svc = crate::service::platform_service();\n *587│    match crate::service::plan_stop(is_running(), svc.detected(), svc.is_active()) {\n  588│        crate::service::StopPlan::NotRunning => Ok(StopOutcome::NotRunning),\n *589│        crate::service::StopPlan::ServiceStop => {\n  590│            svc.stop()?;\n *591│            Ok(StopOutcome::ServiceStopped(svc.label()))\n  592│        }\n  593│        crate::service::StopPlan::IpcStop => {\n *594│            request_stop()?;\n  595│            Ok(StopOutcome::IpcStopped)\n  596│        }\n  597│    }\n     │...\n 1720│            // self-sustaining pin doyle's cdb found feeding the stop-barrier convoy).\n*1721│            let _ = crate::seedmap::request_stop(&name);\n 1722│        });\n 1723│    }\n 1724│}\n### service.rs\n  47│    /// auto-restart-fight the teardown.\n *48│    ServiceStop,\n  49│    /// A manual daemon is running — graceful IPC stop.\n  50│    IpcStop,\n  51│}\n    │...\n  70│// [impl->REQ-DAEMON-6]\n *71│pub fn plan_stop(running: bool, service_detected: bool, service_active: bool) -> StopPlan {\n  72│    if service_detected && service_active {\n *73│        StopPlan::ServiceStop\n  74│    } else if running {\n  75│        StopPlan::IpcStop\n  76│    } else {\n    │...\n 379│    #[test]\n*380│    fn plan_stop_routes_managed_then_ipc_then_noop() {\n*381│        assert_eq!(plan_stop(true, true, true), StopPlan::ServiceStop);\n*382│        assert_eq!(plan_stop(true, true, false), StopPlan::IpcStop);\n*383│        assert_eq!(plan_stop(true, false, false), StopPlan::IpcStop);\n*384│        assert_eq!(plan_stop(false, false, false), StopPlan::NotRunning);\n 385│        // detected+inactive, nothing running ⇒ nothing to stop.\n*386│        assert_eq!(plan_stop(false, true, false), StopPlan::NotRunning);\n 387│    }\n 388│\n 389│    // [unit->REQ-DAEMON-7] `run` refuses only when it WOULD VANISH: on Windows\n### livehost.rs\n  417│/// action behind BOTH the wake-resume leg ([`resume_woken_endpoint`]) and the\n *418│/// daemon-restart survival pass ([`resume_restart_orphaned_endpoints`]). `reason_tag`\n  419│/// names the trigger in the log breadcrumb (`WAKE` / `DAEMON_RESTART`), scoped so an\n  420│/// operator can tell a woken resume from a restart-survival re-run. Records the custody\n  421│/// guard pid + clears any stale `host_error` on success; stamps `host_error` on\n     │...\n  549│// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n *550│pub fn resume_restart_orphaned_endpoints(\n  551│    owlery: &Path,\n  552│    registered: &[(AdapterRecord, Manifest)],\n  553│    adapters_dir: &Path,\n     │...\n 1002│            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n*1003│            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 1004│        }\n 1005│        while !stop.load(Ordering::SeqCst) {\n 1006│            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n### autostart.rs\n  36│// [impl->REQ-ENDPOINT-AUTOSTART]\n *37│pub fn replay_startup_endpoints(broker_name: &str) {\n  38│    let entries = DaemonConfig::load().startup_endpoints;\n  39│    if entries.is_empty() {\n  40│        return;\n    │...\n 126│\n*127│    /// Run `replay_startup_endpoints` on a watchdog thread and require it to\n 128│    /// RETURN within `budget` — a replay is best-effort and must never hang the\n 129│    /// daemon-boot thread. The socket name is intentionally bogus: these gates\n 130│    /// exercise only the pre-launch legs (empty list / loud-skip), which never\n    │...\n 134│        let handle = std::thread::spawn(move || {\n*135│            replay_startup_endpoints(\"bogus-broker-socket-never-dialed\");\n 136│            let _ = tx.send(());\n 137│        });\n 138│        rx.recv_timeout(budget).expect(\n*139│            \"replay_startup_endpoints must return promptly on the pre-launch paths \\\n 140│             (empty list / unresolved adapter loud-skip), never hang the boot thread\",\n 141│        );\n 142│        handle.join().expect(\"watchdog thread must not panic\");"},"isError":false,"timestamp":1784195808753}}
{"type":"message","id":"73484a6c","parentId":"359b1159","timestamp":"2026-07-16T09:56:48.775Z","message":{"role":"toolResult","toolCallId":"call_ztJpY7fUEnhZEfmzytuKs3yH|fc_0cc7a0c9a797f232016a58aae0a988819a8650baf2a3bdb183","toolName":"read","content":[{"type":"text","text":"## [0.31.0] - 2026-07-10\n…\n### Fixed\n…\n- A message could be delivered twice — once to the agent and once typed into\n…\n- A stale or leaked identity value inherited through the environment could let\n  a new session sit down in an agent seat that was not its own. The daemon now\n  scrubs inherited identity variables at startup and refuses seat takeovers\n  from sessions that do not hold the seat.\n- Connection logs blamed the wrong thing: routine dial noise from peers that\n  went offline was reported with the same message as a genuinely stuck\n  connection being cut off. The two cases now log distinctly, and connection\n  log lines carry which session and endpoint were involved, so a real stall is\n  recognizable at a glance instead of drowned in noise.\n\n## [0.30.6] - 2026-07-10\n\nA reliability patch closing the last update-wedge failure mode: a stuck session\nconsumer can no longer freeze the daemon under load.\n\n> **Applying this update needs a daemon restart.** Unlike the recent seamless\n> in-place updates, this fix lives in the always-on daemon, so `spt update fetch\n> --apply` lands it via a full daemon restart rather than a live swap — your\n> sessions restart as the daemon comes back. This is a one-time cost to install\n> the fix.\n\n### Fixed\n\n- Under load, if a session's controller consumer stopped reading (for example a\n  wedged `spt rc --take` that stalled without disconnecting), the daemon could\n  block indefinitely trying to write to it — freezing live terminal sessions with\n  no recovery short of restarting the box. Every write the daemon makes to a\n  session connection is now time-bounded and cancelable: a stuck consumer is cut\n  loose instead of taking the whole daemon down, so your other sessions keep\n  streaming and a fresh `spt rc` resumes cleanly.\n\n## [0.30.5] - 2026-07-09\n\nA reliability patch completing the in-place-update fix: applying an update no longer\nfreezes your live terminal sessions.\n\n### Fixed\n\n- Applying an update in place with `spt update fetch --apply` could still freeze all of\n  your live terminal sessions a few seconds after the swap — they stopped streaming and\n  `spt rc` could no longer attach, so the box had to be restarted to recover. (The\n  previous release narrowed this but did not fully close it.) Resuming across the update\n  no longer floods the daemon's internal channel, so your open sessions keep streaming and\n  `spt rc` keeps working straight through the swap — no freeze, no restart. (Because the\n  fix lives in the incoming version, updating *to* this release is what makes your next\n  in-place update seamless.)\n\n## [0.30.4] - 2026-07-09\n\nA follow-up reliability patch for in-place updates: resumed sessions are no longer\nseized during the update handoff.\n\n### Fixed\n\n- Applying an update in place with `spt update fetch --apply` could freeze or seize\n  control of your live terminal sessions as the daemon restarted — a resumed session\n…\n  brain swap. (Follow-up to the previous release's seamless-swap fix.)\n\n\n[Showing lines 184-238 of 1081. Use :239 to continue]"}],"details":{"truncation":{"content":"  a new session sit down in an agent seat that was not its own. The daemon now\r\n  scrubs inherited identity variables at startup and refuses seat takeovers\r\n  from sessions that do not hold the seat.\r\n- Connection logs blamed the wrong thing: routine dial noise from peers that\r\n  went offline was reported with the same message as a genuinely stuck\r\n  connection being cut off. The two cases now log distinctly, and connection\r\n  log lines carry which session and endpoint were involved, so a real stall is\r\n  recognizable at a glance instead of drowned in noise.\r\n\r\n## [0.30.6] - 2026-07-10\r\n\r\nA reliability patch closing the last update-wedge failure mode: a stuck session\r\nconsumer can no longer freeze the daemon under load.\r\n\r\n> **Applying this update needs a daemon restart.** Unlike the recent seamless\r\n> in-place updates, this fix lives in the always-on daemon, so `spt update fetch\r\n> --apply` lands it via a full daemon restart rather than a live swap — your\r\n> sessions restart as the daemon comes back. This is a one-time cost to install\r\n> the fix.\r\n\r\n### Fixed\r\n\r\n- Under load, if a session's controller consumer stopped reading (for example a\r\n  wedged `spt rc --take` that stalled without disconnecting), the daemon could\r\n  block indefinitely trying to write to it — freezing live terminal sessions with\r\n  no recovery short of restarting the box. Every write the daemon makes to a\r\n  session connection is now time-bounded and cancelable: a stuck consumer is cut\r\n  loose instead of taking the whole daemon down, so your other sessions keep\r\n  streaming and a fresh `spt rc` resumes cleanly.\r\n\r\n## [0.30.5] - 2026-07-09\r\n\r\nA reliability patch completing the in-place-update fix: applying an update no longer\r\nfreezes your live terminal sessions.\r\n\r\n### Fixed\r\n\r\n- Applying an update in place with `spt update fetch --apply` could still freeze all of\r\n  your live terminal sessions a few seconds after the swap — they stopped streaming and\r\n  `spt rc` could no longer attach, so the box had to be restarted to recover. (The\r\n  previous release narrowed this but did not fully close it.) Resuming across the update\r\n  no longer floods the daemon's internal channel, so your open sessions keep streaming and\r\n  `spt rc` keeps working straight through the swap — no freeze, no restart. (Because the\r\n  fix lives in the incoming version, updating *to* this release is what makes your next\r\n  in-place update seamless.)\r\n\r\n## [0.30.4] - 2026-07-09\r\n\r\nA follow-up reliability patch for in-place updates: resumed sessions are no longer\r\nseized during the update handoff.\r\n\r\n### Fixed\r\n\r\n- Applying an update in place with `spt update fetch --apply` could freeze or seize\r\n  control of your live terminal sessions as the daemon restarted — a resumed session\r","truncated":true,"truncatedBy":"lines","totalLines":898,"totalBytes":2779,"outputLines":55,"outputBytes":2779,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"## [0.31.0] - 2026-07-10\n…\n### Fixed\n…\n- A message could be delivered twice — once to the agent and once typed into\n…\n- A stale or leaked identity value inherited through the environment could let\n  a new session sit down in an agent seat that was not its own. The daemon now\r\n  scrubs inherited identity variables at startup and refuses seat takeovers\r\n  from sessions that do not hold the seat.\r\n- Connection logs blamed the wrong thing: routine dial noise from peers that\r\n  went offline was reported with the same message as a genuinely stuck\r\n  connection being cut off. The two cases now log distinctly, and connection\r\n  log lines carry which session and endpoint were involved, so a real stall is\r\n  recognizable at a glance instead of drowned in noise.\r\n\r\n## [0.30.6] - 2026-07-10\r\n\r\nA reliability patch closing the last update-wedge failure mode: a stuck session\r\nconsumer can no longer freeze the daemon under load.\r\n\r\n> **Applying this update needs a daemon restart.** Unlike the recent seamless\r\n> in-place updates, this fix lives in the always-on daemon, so `spt update fetch\r\n> --apply` lands it via a full daemon restart rather than a live swap — your\r\n> sessions restart as the daemon comes back. This is a one-time cost to install\r\n> the fix.\r\n\r\n### Fixed\r\n\r\n- Under load, if a session's controller consumer stopped reading (for example a\r\n  wedged `spt rc --take` that stalled without disconnecting), the daemon could\r\n  block indefinitely trying to write to it — freezing live terminal sessions with\r\n  no recovery short of restarting the box. Every write the daemon makes to a\r\n  session connection is now time-bounded and cancelable: a stuck consumer is cut\r\n  loose instead of taking the whole daemon down, so your other sessions keep\r\n  streaming and a fresh `spt rc` resumes cleanly.\r\n\r\n## [0.30.5] - 2026-07-09\r\n\r\nA reliability patch completing the in-place-update fix: applying an update no longer\r\nfreezes your live terminal sessions.\r\n\r\n### Fixed\r\n\r\n- Applying an update in place with `spt update fetch --apply` could still freeze all of\r\n  your live terminal sessions a few seconds after the swap — they stopped streaming and\r\n  `spt rc` could no longer attach, so the box had to be restarted to recover. (The\r\n  previous release narrowed this but did not fully close it.) Resuming across the update\r\n  no longer floods the daemon's internal channel, so your open sessions keep streaming and\r\n  `spt rc` keeps working straight through the swap — no freeze, no restart. (Because the\r\n  fix lives in the incoming version, updating *to* this release is what makes your next\r\n  in-place update seamless.)\r\n\r\n## [0.30.4] - 2026-07-09\r\n\r\nA follow-up reliability patch for in-place updates: resumed sessions are no longer\r\nseized during the update handoff.\r\n\r\n### Fixed\r\n\r\n- Applying an update in place with `spt update fetch --apply` could freeze or seize\r\n  control of your live terminal sessions as the daemon restarted — a resumed session\r\n…\n  brain swap. (Follow-up to the previous release's seamless-swap fix.)\n","startLine":137,"lineNumbers":[137,null,158,null,160,null,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,null,242,243]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\CHANGELOG.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1081,"totalBytes":2779,"outputLines":55,"outputBytes":2779,"shownRange":{"start":184,"end":238},"nextOffset":239}}},"isError":false,"timestamp":1784195808775}}
{"type":"message","id":"b4bd4ec4","parentId":"73484a6c","timestamp":"2026-07-16T09:56:48.821Z","message":{"role":"toolResult","toolCallId":"call_N2D6vJ61GmasVdgQFK8QYXRU|fc_0cc7a0c9a797f232016a58aae0a980819aaa51cca923baf07b","toolName":"read","content":[{"type":"text","text":"### 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach  `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]`\n…\n- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`) and the inject floor (`[REQ-HAZARD-INJECT-WORKER-POISON]`) were fixed one lock at a time; the SURVIVING class was the attach-path locks — the session map (`sessions` ×18 + its `sessions_exit` alias), the per-session `OutputLog` ring (×1…\n…\n- **Source:** REMOTE-TRUTH B-1 (pivoted — the triage's named floor sites were already closed by `[REQ-HAZARD-INJECT-WORKER-POISON]`; the surviving attach-path lock class is the real root), doyle B-1 ruling (recover all three via one helper + the OutputLog ring coherence clamp).\n\n### 7.34 A dead `rec.pid` on an spt-hosted perch is EXPECTED — no reader may alive-gate on `rec.pid` alone  `[REQ-HAZARD-DEAD-REC-PID]`\n<!-- [doc->REQ-HAZARD-DEAD-REC-PID] -->\n- **Failure (paid-for, field evidence — the F-026 #11 dead-pid class):** an spt-hosted endpoint's `rec.pid` records the ephemeral bind-CLI pid, which dies IMMEDIATELY after bind (the broker holds the PTY; there is no resident harness process at that pid). Readers that alive-gated on `rec.pid` alone treated the perch as stale: self-detect leg (c) could NEVER resolve an spt-hosted sender (its from-stamp degraded to `cli@NODE`, operator #7, and replies bounced `NO_PERCH` — sighted on hall-bf/ball-b, v0.24.0).\n- **Invariant:** a dead `rec.pid` on an spt-hosted perch is an EXPECTED state, not staleness. NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed `status` field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution, where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (ADR-0021 seed-hint discipline: pid is a bind-time SEED, not a truth anchor — re-anchoring truth in `rec.pid` is overruled by design). Any newly sighted `rec.pid`-alive-gating reader gets the same scoped fix and extends THIS requirement's evidence — no new REQ per reader.\n- **spt-core mapping:** `roster.rs` `detect_self_by_ancestry` (the first sighted reader, fixed by `[REQ-SELF-DETECT-PARENT-PID]`: `parent_pid` candidates alongside `rec.pid`). int = the E-1 red-first (`detect_self_resolves_spt_hosted_perch_via_parent_pid`: dead `rec.pid` + live-ancestor `parent_pid` resolves), dual-tagged as this class's test.\n- **Source:** REMOTE-TRUTH E-1 rider (doyle ruling 2026-07-05: scoped reader-side (b) over re-stamping `rec.pid` (a) — ADR-0021 ground truth, migration hole, blast radius).\n\n### 7.35 The cached ceremony-clock NTP offset must NOT survive an OS clock STEP — an offset measured against the pre-step clock strands every pairing for the TTL  `[REQ-HAZARD-CEREMONY-CLOCK-STEP]`\n- **Failure (paid-for, field evidence — the enlyzeam BIGNET-join RCA 2026-07-06):** enlyzeam's `w32time` was STOPPED, so its system clock drifted +3m30s; the pairing ceremony offset (REQ-PAIR-8, `ntp.rs`) correctly corrected the TOTP clock — until an operator `w32tm /resync` STEPPED the OS clock back −210s under the LIVE daemon. The cached offset had been measured against the OLD (drifted) clock and its snapshot was monotonic-only (`when: Option<Instant>`), so the step was invisible to the cache: for up to the 15-min TTL the ceremony clock stayed ~7 TOTP steps in the past, every `subnet join` returned NO_SEED_HOLDER while fresh-process probes met in <500ms, and only a daemon bounce (which forced a fresh query) healed it. Timeline-proven: refresh cadence 15:0…\n- **Invariant:** the offset cache MUST detect an OS clock step and re-query, never apply a stale offset across it. The snapshot stores an `(Instant, SystemTime)` PAIR; on every read, if the wall clock diverged from the monotonic clock since the snapshot beyond a small tolerance (`|wall_elapsed − mono_elapsed| > ~2s`) the OS clock STEPPED ⇒ force an immediate re-query (offset recomputed against the new clock) regardless of TTL. Belt-and-braces at the join edge: `meet_seed_holder`, on search-deadline exhaustion, forces ONE fresh NTP query and runs one final sweep at the corrected step before the NO_SEED_HOLDER verdict — so a stepped-clock join self-heals WITHOUT a daemon bounce. The step tolerance sits far below one TOTP step (30 s) and well above scheduling j…\n- **spt-core mapping:** `spt_net::net::pairing::ntp` — `CachedOffset{when, wall_at}` pair + `refresh_needed`/`clock_stepped`/`signed_secs` (step-detect) + `force_refresh`/`invalidate` (the meet-exhaustion re-query seam); `spt_daemon::pairhost::meet_seed_holder` via `sweep_then_final_retry` (force-refresh + one final sweep on exhaustion). unit = `clock_step_forces_refresh_via_injected_reads` (injected mono/wall reads: a wall step forward OR back with mono barely moved re-queries; agreeing clocks within TTL do not; TTL-expiry and never-queried also refresh — no 15-min sleep) + `invalidate_clears_the_snapshot` + `meet_retries_once_after_refresh_on_exhaustion`/`meet_final_retry_failing_surrenders_once` (exactly one post-refresh retry, then surrender with the ric…\n- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 multihome + D2 loud-fail + D4 verbose-clock the siblings).\n\n### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a controller BLOCKED (not exited) on an IDLE session was evicted by NEITHER. Within seconds: attached output froze, `detach` did not release the control stamp (release routed through the wedged conn), reattach was REFUSED (`controlled-by` latched), `rc --take` hung, and `d…\n- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stall-evicts it. Eviction is triggered TIME-based, not output-driven: at the take/reattach path (`resolve_subscribe`, so control self-heals against broker truth even on an idle session) AND at the `KIND_SESSIONS` reap. (2) The stamp release is BROKER-side — `clear_c…\n- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (`KIND_STALL_EVICTS` IPC); `brain.rs` `Brain::stall_evicts`; `cli.rs` `render_stall_evict_line` (daemon-status surface, public wording). unit = `controller_write_stall_predicate_distinguishes_wedged_from_idle` + `stalled_incumbent_is_evicted_on_reattach_and_cont…\n- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n\n### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\n- **Failure (paid-for, field-CONFIRMED on 0.29.0 — doyle diagnosis + perri screenshot `WindowsTerminal_6iSjya8pMt.png`):** the shipped W5-A settle-gate (7-of-W5, `REQ-INJECT-MULTILINE-INTEGRITY`) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the premise that the head-swallow race is a STARTUP condition (the input reader not yet attached after spawn). That premise is FALSE: a mid-session `/clear` re-enters the harness's raw-mode input reader, re-attaching it and RE-CREATING the pre-settle window — but the one-shot had already fired at spawn, so `settle_before_inject` was SKIPPED and the head was eaten again. A checkpoint-wake payload injected right after `/clear` arrived head-truncated (mid-path `spt/Cargo.toml)`); echo-verify (Layer 2) is …\n- **Invariant:** the settle-gate re-arms before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY — the bug-prone class, where re-settling is cheap (a tick or two once the reader answers) and necessary (a mid-session reader reattach must be re-confirmed). The steady-state settle is latch-skipped ONLY where the probe is known UNOBSERVABLE on this PTY (a non-echoing ConPTY whose DSR answer never surfaces): there is no reader-reattach race to guard, and each settle would burn the full `INJECT_SETTLE` deadline. The class is discriminated by the settle's OWN return value — `settle_before_inject` returns `true` iff the session ring advanced (probe observed) — latched on the FIRST attempt only; a RE-DRIVE (`attempt > 1`, reached only on a swallowed head) AL…\n- **spt-core mapping:** `broker.rs` `run_inject_worker` — the `settled_once: bool` one-shot is replaced by a `probe_unobservable: bool` latch (init `false` = assume observable → settle first delivery); the gate is the pure `should_settle(attempt, probe_unobservable) = attempt > 1 || !probe_unobservable`, and the first-attempt settle's `observed` return sets `probe_unobservable = !observed`. `settle_before_inject` gains a `bool` return (observed vs timed-out). unit = `should_settle_rearms_on_observable_pty` (observable PTY re-settles each delivery; unobservable-probe PTY skips the steady-state settle; a re-drive settles in both classes — RED-first: restore the one-shot and the observable-PTY assert flips). No int edit: the `inject_control_wedge.rs` mocks (`fi…\n- **Source:** post-0.29.0 field fix (doyle diagnosis + design ruling `BUILD-SPEC-inject-settle-rearm`; todlando build 2026-07-08). An impl bug in the shipped W5-A Layer-1 fix, not a new invariant class — the settle-gate was correct but under-armed.\n\n### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefinitely** (~127.95 s in the capture, released only when a brain restart tore the connections down). The 7.36 logical `stall_evict_controller` removed the controller ROLE but neither canceled the in-flight pipe write, closed the physical connection, nor called `Cance…\n- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On deadline / partial write / cancellation / unknown completion: (1) poison the whole physical conn idempotently, (2) abort read+write so `handle_conn` reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer, (4) NEV…\n- **spt-core mapping:** `spt-daemon/src/conn.rs` `BrokerConn` (Condvar-gated serialized write gate + poisoned state + per-in-flight op id + watchdog + `abort_physical()`: cfg(windows) `CancelIoEx` → completion handshake → `DisconnectNamedPipe` raw kernel32 externs; cfg(unix) `UnixStream::shutdown(Both)`); `broker.rs` `SharedSend = Arc<BrokerConn>` + `controller_writer`/`viewer_writer`/`send_frame`/`send_error` routed; `nethost.rs` stream-log/presence sends routed. int = `brain_decouple.rs` `non_draining_controller_stall_evict_releases_writer_and_connection` (`#[cfg(windows)]`, the hertz 9-step: real PTY burst, black-holed remote controller, exactly one logical stall-evict via an independent draining conn, the black-holed conn's `handle_conn` finishes ≤2 s of…\n- **Source:** UPDATE-WEDGE round-4 (hertz root doc `docs/UPDATE-WEDGE-ROOT-CAUSE.md` @f8596ca; doyle gate ruling `docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md`; todlando build 2026-07-09). The residual 7.36 left open: logical eviction without physical retirement.\n\n---\n\n### 7.39 Per-session identity env (`SPT_ENDPOINT_ID`/`OWL_SESSION_ID`/`SPT_AGENT_ID`) is NEVER inherited — the daemon scrubs it at startup AND on every role spawn, regardless of any role's declared `env_remove`  `[REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]`\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\n…\n\n\n[Showing lines 609-648 of 736. Use :649 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"- **Source:** REMOTE-TRUTH B-1 (pivoted — the triage's named floor sites were already closed by `[REQ-HAZARD-INJECT-WORKER-POISON]`; the surviving attach-path lock class is the real root), doyle B-1 ruling (recover all three via one helper + the OutputLog ring coherence clamp).\r\n\r\n### 7.34 A dead `rec.pid` on an spt-hosted perch is EXPECTED — no reader may alive-gate on `rec.pid` alone  `[REQ-HAZARD-DEAD-REC-PID]`\r\n<!-- [doc->REQ-HAZARD-DEAD-REC-PID] -->\r\n- **Failure (paid-for, field evidence — the F-026 #11 dead-pid class):** an spt-hosted endpoint's `rec.pid` records the ephemeral bind-CLI pid, which dies IMMEDIATELY after bind (the broker holds the PTY; there is no resident harness process at that pid). Readers that alive-gated on `rec.pid` alone treated the perch as stale: self-detect leg (c) could NEVER resolve an spt-hosted sender (its from-stamp degraded to `cli@NODE`, operator #7, and replies bounced `NO_PERCH` — sighted on hall-bf/ball-b, v0.24.0).\r\n- **Invariant:** a dead `rec.pid` on an spt-hosted perch is an EXPECTED state, not staleness. NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed `status` field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution, where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (ADR-0021 seed-hint discipline: pid is a bind-time SEED, not a truth anchor — re-anchoring truth in `rec.pid` is overruled by design). Any newly sighted `rec.pid`-alive-gating reader gets the same scoped fix and extends THIS requirement's evidence — no new REQ per reader.\r\n- **spt-core mapping:** `roster.rs` `detect_self_by_ancestry` (the first sighted reader, fixed by `[REQ-SELF-DETECT-PARENT-PID]`: `parent_pid` candidates alongside `rec.pid`). int = the E-1 red-first (`detect_self_resolves_spt_hosted_perch_via_parent_pid`: dead `rec.pid` + live-ancestor `parent_pid` resolves), dual-tagged as this class's test.\r\n- **Source:** REMOTE-TRUTH E-1 rider (doyle ruling 2026-07-05: scoped reader-side (b) over re-stamping `rec.pid` (a) — ADR-0021 ground truth, migration hole, blast radius).\r\n\r\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]`\r\n- **Failure (paid-for, field evidence — the enlyzeam BIGNET-join RCA 2026-07-06):** enlyzeam's `w32time` was STOPPED, so its system clock drifted +3m30s; the pairing ceremony offset (REQ-PAIR-8, `ntp.rs`) correctly corrected the TOTP clock — until an operator `w32tm /resync` STEPPED the OS clock back −210s under the LIVE daemon. The cached offset had been measured against the OLD (drifted) clock and its snapshot was monotonic-only (`when: Option<Instant>`), so the step was invisible to the cache: for up to the 15-min TTL the ceremony clock stayed ~7 TOTP steps in the past, every `subnet join` returned NO_SEED_HOLDER while fresh-process probes met in <500ms, and only a daemon bounce (which forced a fresh query) healed it. Timeline-proven: refresh cadence 15:0…\n- **Invariant:** the offset cache MUST detect an OS clock step and re-query, never apply a stale offset across it. The snapshot stores an `(Instant, SystemTime)` PAIR; on every read, if the wall clock diverged from the monotonic clock since the snapshot beyond a small tolerance (`|wall_elapsed − mono_elapsed| > ~2s`) the OS clock STEPPED ⇒ force an immediate re-query (offset recomputed against the new clock) regardless of TTL. Belt-and-braces at the join edge: `meet_seed_holder`, on search-deadline exhaustion, forces ONE fresh NTP query and runs one final sweep at the corrected step before the NO_SEED_HOLDER verdict — so a stepped-clock join self-heals WITHOUT a daemon bounce. The step tolerance sits far below one TOTP step (30 s) and well above scheduling j…\n- **spt-core mapping:** `spt_net::net::pairing::ntp` — `CachedOffset{when, wall_at}` pair + `refresh_needed`/`clock_stepped`/`signed_secs` (step-detect) + `force_refresh`/`invalidate` (the meet-exhaustion re-query seam); `spt_daemon::pairhost::meet_seed_holder` via `sweep_then_final_retry` (force-refresh + one final sweep on exhaustion). unit = `clock_step_forces_refresh_via_injected_reads` (injected mono/wall reads: a wall step forward OR back with mono barely moved re-queries; agreeing clocks within TTL do not; TTL-expiry and never-queried also refresh — no 15-min sleep) + `invalidate_clears_the_snapshot` + `meet_retries_once_after_refresh_on_exhaustion`/`meet_final_retry_failing_surrenders_once` (exactly one post-refresh retry, then surrender with the ric…\n- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 multihome + D2 loud-fail + D4 verbose-clock the siblings).\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","truncated":true,"truncatedBy":"lines","totalLines":128,"totalBytes":19976,"outputLines":40,"outputBytes":19976,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"### 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach  `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]`\n…\n- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`) and the inject floor (`[REQ-HAZARD-INJECT-WORKER-POISON]`) were fixed one lock at a time; the SURVIVING class was the attach-path locks — the session map (`sessions` ×18 + its `sessions_exit` alias), the per-session `OutputLog` ring (×1…\n…\n- **Source:** REMOTE-TRUTH B-1 (pivoted — the triage's named floor sites were already closed by `[REQ-HAZARD-INJECT-WORKER-POISON]`; the surviving attach-path lock class is the real root), doyle B-1 ruling (recover all three via one helper + the OutputLog ring coherence clamp).\r\n\r\n### 7.34 A dead `rec.pid` on an spt-hosted perch is EXPECTED — no reader may alive-gate on `rec.pid` alone  `[REQ-HAZARD-DEAD-REC-PID]`\r\n<!-- [doc->REQ-HAZARD-DEAD-REC-PID] -->\r\n- **Failure (paid-for, field evidence — the F-026 #11 dead-pid class):** an spt-hosted endpoint's `rec.pid` records the ephemeral bind-CLI pid, which dies IMMEDIATELY after bind (the broker holds the PTY; there is no resident harness process at that pid). Readers that alive-gated on `rec.pid` alone treated the perch as stale: self-detect leg (c) could NEVER resolve an spt-hosted sender (its from-stamp degraded to `cli@NODE`, operator #7, and replies bounced `NO_PERCH` — sighted on hall-bf/ball-b, v0.24.0).\r\n- **Invariant:** a dead `rec.pid` on an spt-hosted perch is an EXPECTED state, not staleness. NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed `status` field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution, where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (ADR-0021 seed-hint discipline: pid is a bind-time SEED, not a truth anchor — re-anchoring truth in `rec.pid` is overruled by design). Any newly sighted `rec.pid`-alive-gating reader gets the same scoped fix and extends THIS requirement's evidence — no new REQ per reader.\r\n- **spt-core mapping:** `roster.rs` `detect_self_by_ancestry` (the first sighted reader, fixed by `[REQ-SELF-DETECT-PARENT-PID]`: `parent_pid` candidates alongside `rec.pid`). int = the E-1 red-first (`detect_self_resolves_spt_hosted_perch_via_parent_pid`: dead `rec.pid` + live-ancestor `parent_pid` resolves), dual-tagged as this class's test.\r\n- **Source:** REMOTE-TRUTH E-1 rider (doyle ruling 2026-07-05: scoped reader-side (b) over re-stamping `rec.pid` (a) — ADR-0021 ground truth, migration hole, blast radius).\r\n\r\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]`\r\n- **Failure (paid-for, field evidence — the enlyzeam BIGNET-join RCA 2026-07-06):** enlyzeam's `w32time` was STOPPED, so its system clock drifted +3m30s; the pairing ceremony offset (REQ-PAIR-8, `ntp.rs`) correctly corrected the TOTP clock — until an operator `w32tm /resync` STEPPED the OS clock back −210s under the LIVE daemon. The cached offset had been measured against the OLD (drifted) clock and its snapshot was monotonic-only (`when: Option<Instant>`), so the step was invisible to the cache: for up to the 15-min TTL the ceremony clock stayed ~7 TOTP steps in the past, every `subnet join` returned NO_SEED_HOLDER while fresh-process probes met in <500ms, and only a daemon bounce (which forced a fresh query) healed it. Timeline-proven: refresh cadence 15:0…\n- **Invariant:** the offset cache MUST detect an OS clock step and re-query, never apply a stale offset across it. The snapshot stores an `(Instant, SystemTime)` PAIR; on every read, if the wall clock diverged from the monotonic clock since the snapshot beyond a small tolerance (`|wall_elapsed − mono_elapsed| > ~2s`) the OS clock STEPPED ⇒ force an immediate re-query (offset recomputed against the new clock) regardless of TTL. Belt-and-braces at the join edge: `meet_seed_holder`, on search-deadline exhaustion, forces ONE fresh NTP query and runs one final sweep at the corrected step before the NO_SEED_HOLDER verdict — so a stepped-clock join self-heals WITHOUT a daemon bounce. The step tolerance sits far below one TOTP step (30 s) and well above scheduling j…\n- **spt-core mapping:** `spt_net::net::pairing::ntp` — `CachedOffset{when, wall_at}` pair + `refresh_needed`/`clock_stepped`/`signed_secs` (step-detect) + `force_refresh`/`invalidate` (the meet-exhaustion re-query seam); `spt_daemon::pairhost::meet_seed_holder` via `sweep_then_final_retry` (force-refresh + one final sweep on exhaustion). unit = `clock_step_forces_refresh_via_injected_reads` (injected mono/wall reads: a wall step forward OR back with mono barely moved re-queries; agreeing clocks within TTL do not; TTL-expiry and never-queried also refresh — no 15-min sleep) + `invalidate_clears_the_snapshot` + `meet_retries_once_after_refresh_on_exhaustion`/`meet_final_retry_failing_surrenders_once` (exactly one post-refresh retry, then surrender with the ric…\n- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 multihome + D2 loud-fail + D4 verbose-clock the siblings).\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…\n","startLine":604,"lineNumbers":[604,null,606,null,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,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":19976,"outputLines":40,"outputBytes":19976,"shownRange":{"start":609,"end":648},"nextOffset":649},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195808821}}
{"type":"compaction","id":"b66f1235","parentId":"b4bd4ec4","timestamp":"2026-07-16T09:56:49.041Z","summary":"You are resuming a prior conversation. Its earlier turns were archived to reclaim context and are reproduced under HISTORY below, oldest to newest. Read HISTORY in full, then continue from the live conversation that follows it.\n\nThe archived transcript is compact: each turn opens with a heading — `# User ¶`, `# Assistant ¶`, or `# Tool call ¶` — assistant reasoning is wrapped in _italics_, and tool output sits inside `<out>…</out>`.\n\nReading HISTORY:\n- Plain-text sections are the verbatim transcript — rely on them exactly.\n- Some middle sections are attached as images instead of text. Each image is a page of that same transcript and belongs at its place in the reading order, between marked delimiters. Within an image, a solid black cell marks a newline and runs of spaces collapse to one.\n  - A frame is one grid 196 characters wide and up to 71 rows tall: read left to right, top to bottom — there is no word wrap, so a word may break across rows.\n  - Text inside `<out>` is dim gray — that gray is archived tool output, not conversation.\n- When an exact earlier detail matters and a section reads unclearly, re-derive it from the workspace (re-read files, re-run commands) rather than guessing.\n\nFILES\n===================\nCONTEXT.md (Read)\n# crates/\n## spt-daemon/\n### src/\napplyhost.rs (Read)\nattach.rs (Read)\nbrain.rs (Read)\nbrainproc.rs (Read)\nbroker.rs (Read)\nconn.rs (Read)\ndaemon.rs (Read)\n### tests/\nbrain_resume_conn_deadlock.rs (Read)\nfalse_promote.rs (Read)\n## spt/\n### src/\ncli.rs (Read)\nrc.rs (Read)\n### tests/\ndaemon_refresh_e2e.rs (Read)\n# docs/\nDEBUG-ROLLOUT.md (Read)\nKNOWN-HAZARDS.md (Read)\nNEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md (Read)\nUPDATE-WEDGE-2-ROUND3-CODEREAD.md (Read)\nUPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md (Read)\n## adr/\n0004-single-daemon-broker-brain-split-and-self-update.md (Read)\n0018-broker-brain-process-isolation-restoration.md (Read)\n[…1 files elided…]\n\nHISTORY\n===================","shortSummary":"Archived 82,906 chars of history onto 4 snapcompact frames (+27,833 chars as text)","firstKeptEntryId":"a1dcc99c","tokensBefore":234920,"details":{"readFiles":["CONTEXT.md","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt/src/cli.rs","crates/spt/src/rc.rs","crates/spt/tests/daemon_refresh_e2e.rs","docs/DEBUG-ROLLOUT.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/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md"],"modifiedFiles":[]},"fromExtension":false,"preserveData":{"snapcompact":{"frames":[{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13325,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"}],"totalChars":82906,"truncatedChars":0,"text":"# User ¶█Complete the assignment below, thoroughly:█# Target█Daemon refresh/update implementation and process-lifecycle boundaries; exact symbols and call chain. Non-goal: general update review.█# Change█Trace `spt daemon refresh` and `spt update` through CLI, service manager, daemon brain, broker, inherited handles, and restart/refresh behavior. Identify operations capable of invalidating or pausing existing PTY controller streams while leaving broker PTYs alive. Rank falsifiable candidates with evidence.█# Acceptance█Report exact line-anchored symbols, process transitions, likely race windows, and existing focused tests or missing test seams. No writes or commands that alter runtime state.█# Assistant ¶█_**Planning skill and doc reads**_█# 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 lifecycle sources█glob(path=\"**/*.{ts,tsx,js,mjs,cjs,md,json}\", hidden=true, gitignore=true, limit=500)█<out>█\u000eCHANGELOG.md█CONTEXT.md█README.md█CLAUDE.md█V0.19.0-P6-SCREEN-GRID-DESIGN.md█V0.17.0-ROBUST-JOIN-AND-PRESENCE-JIT.md█V0.16.0-UPDATE-ARC-AND-CLI-JIT.md█AGENTS.md█V0.15.0-PLAN.md█ROADMAP.md█F-019-TRANSLATION-BINARY-INSTALL-DIR-RESOLVE-DESIGN.md█V0.14.1-HOME-SUBNET-PICKER-LAYER-DESIGN.md█F-018-ADAPTER-ADD-NONDESTRUCTIVE-DESIGN.md█V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md█W3-DESIGN-PROPOSAL.md█W3-GATE-VERDICT.md█W3-DESIGN-GATE.md█V0.13.2-ADAPTER-PACKAGING-JIT.md█V0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md█V0.13.0-W4-PICKER-UX-PLAN.md█V0.13.0-VIEWER-RING-ROLL-SNAP-JIT.md█V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md█V0.13.0-SKIP-TO-LIVE-JIT.md█V0.13.0-RC-VT-TRANSLATE-DESIGN.md█V0.13.0-P2-SESSION-RESUME-DESIGN.md█V0.13.0-P1C-CONTROLLER-WRITER-REORDER-PLAN.md█V0.13.0-P1-RC-PASTE-DESIGN.md█V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md█V0.13.0-P0-JIT-PLAN.md█V0.13.0-DELIVERY-CONTROL-JIT.md█V0.13.0-BUG2-RC-VT-TRANSLATE-PLAN.md█V0.12.1-LIFECYCLE-JIT.md█V074-PLAN.md█M11-PLAN.md█DAEMON-LIFECYCLE-COMPLETION-PLAN.md█REQ-MSG-ENVELOPE-PLAN.md█PRD.md█M12-W4-RULING.md█M12-W5-PLAN.md█M12-W4-PLAN.md█M12-W3-PLAN.md█M12-W3-RULING.md█M12-W2-RULING.md█M12-W2.5-PLAN.md█M12-W2.5-RULING.md█M12-W2-PLAN.md█M12-W1-PLAN.md█M12-W1.5-PLAN.m [...3628ch elided...] -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█## design/█subnet-presence-display.md█robust-wan-subnet-join.md█v0.16.0-update-arc-and-cli.md█## spikes/█SPIKE-06-idempotent-boundary.md█SPIKE-07-branchstore.md█SPIKE-04-forkpty-parity.md█SPIKE-05-restart-stress.md█SPIKE-03-quic-survival.md█# crates/spt-runtime/█manifest.schema.json█# releases-repo/█SYNC.md█README.md█[200 results limit reached. Use limit=400 for more]\u000f█</out>█# Tool call ¶█//Locating refresh update paths█grep(pattern=\"daemon refresh|\\\\brefresh\\\\b|\\\\bupdate\\\\b|restart|broker|brain\", path=\"src;packages;test;tests;docs;CONTEXT.md\", 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- [...48705ch elided...] . 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-...█14|█15|## Both legs GREEN█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.█## KNOWN-HAZARDS.md█4|█[Showing lines 1-364 of 725 (49.8KB limit). Use :365 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Reading domain context█read(path=\"CONTEXT.md\", selector=null)█<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**: [...58438ch 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-300 of 883. Use :301 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading known hazards█read(path=\"docs/KNOWN-HAZARDS.md\", selector=null)█<out>█\u000e# Known Hazards█Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** - the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.█**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon<->broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.█---█## 1. Race conditions & ordering█### 1.1 Phantom INIT_SIGNOFF after grace period█- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spoole [...51630ch elided...] structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.█- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.█### 6.2 Soft-cleanup preserves state, removes `ready`█- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.█- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.█- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.█- **Sister cite:** `src/owl/stop.rs`.█...█...█<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->█[Showing lines 1-300 of 736. Use :301 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading rollout guidance█read(path=\"docs/DEBUG-ROLLOUT.md\", selector=null)█<out>█\u000e# Debug rollout runbook█<!-- [doc->REQ-UPD-6] -->█Debug rollout is the maintainer-only fast path for testing a local spt-core█build across a trusted lab subnet. It uses the normal signed self-update█substrate: no raw peer file-copy, no production `spt` CLI surface, and no█embedded debug trust anchor.█Use this when debugging needs a local build to reach multiple lab nodes quickly.█Do not use it for public releases; use `docs/RELEASE-RUNBOOK.md` for that.█## Mental model█- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one█artifact digest per Rust target triple.█- Each recipient verifies the set under its node-local `release-keys.json`,█selects only its own platform artifact, stages it, and then follows the normal█consent/apply policy.█- Debug and stable are separate channels. A debug-pinned node accepts only█`channel = \"debug\"` offers; a stable-pinned node accepts only stable offers.█- Debug versions are monotonic within the debug channel. To recover from a bad█debug build, publish a higher debug version, even if the bytes are a previous█known-good binary.█- A broker-touching candidate may be staged, but apply refuses it while█broker-hel [...2707ch elided...] t triple.█- `WrongChannel` means the node is not pinned to `debug`.█- `Rollback` means the offered debug version is not greater than the node's█current debug-channel version.█- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held█resources; quiesce hosted endpoints first.█## Agent checklist█1. Read `CONTEXT.md` \"debug rollout\" and ADR-0016 before changing the flow.█2. Keep debug tooling in `xtask` or other maintainer tooling, not the public█`spt` CLI/help surface.█3. Keep verification on the production update substrate: signed metadata,█per-platform artifact digest, channel pinning, monotonic version, and█apply-time re-verification.█4. Add or update traceability evidence for `REQ-UPD-6`.█5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.█\u000f█</out>█# Tool call ¶█//Finding architecture decisions█glob(path=\"docs/adr/**/*.md\", hidden=true, gitignore=true, limit=200)█<out>█\u000e# docs/adr/█0037-materialized-project-index.md█0036-private-fork-distribution.md█0005-totp-seeded-spake2-node-pairing.md█0031-server-side-screen-grid-render-repaint.md█0034-per-minter-op-id-namespacing.md█0033-wake-resume-via-reconcile-intent.md█0025-live-daemon-coordinated-adapter-update.md█0022-spt-hosted-idle-delivery-translation-binary.md█0032-boundary-rotation-credential.md█0030-robust-wan-subnet-join.md█0029-adapter-update-arc-resolve-not-execute.md█0028-activity-gated-delivery-and-send-modifier-axes.md█0027-unbound-endpoint-state-and-attach-on-session.md█0026-multi-subnet-home-at-endpoint-run-creation.md█0024-multi-platform-adapter-spt-packaging.md█0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md█0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md█0020-event-envelope-sole-arriving-format-reply-to-removed.md█0012-psyche-trust-boundary-stdout-event-outbound.md█0009-endpoint-access-control-model.md█0008-live-activity-buffer-pty-digest.md█0019-digest-adapter-declared-extractor-session-spanning.md█0018-broker-brain-process-isolation-restoration.md█0016-platform-targeted-update-sets.md█0017-subnet-mesh-membership-seed-proof.md█0014-publish-target-repo-and-lapse-proof-canonical-url.md█0015-release-signing-two-key-trust-anchor.md█0013-psyche-reconciled-context-merge.md█0010-immutable-home-subnet-fork-not-rehome.md█0011-branchstore-git-kv-state-substrate.md█0006-multi-subnet-membership-model.md█0007-subnet-notification-primitive.md█0003-multi-instance-node-anchored-identity-model.md█0004-single-daemon-broker-brain-split-and-self-update.md█0002-bake-networking-into-core-collapse-spt-node.md█0001-clean-room-fork-from-claude-skill-owl.md\u000f█</out>█# Assistant ¶█_**Mapping Rust crates**_█# Tool call ¶█//Mapping Rust lifecycle files█glob(path=\"crates/**/*.rs; tests/**/*.rs\", hidden=true, gitignore=true, limit=1000)█<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█#### fixtures/█git_fixture.rs█gh_fixture.rs█### src/█cli.rs█roster.rs█rc.rs█wansend.rs█#### picker/█data.rs█mod.rs█view.rs█model.rs█#### api/█reporting.rs█startup.rs█auth.rs█mod.rs█worker.rs█delivery.rs█## spt-store/█### src/█projindex.rs█projderive.rs█projinval.rs█lib.rs█branchstore.rs█contextstore.rs█gitrun.rs█psyche_custody.rs█spool.rs█peeraddrs.rs█worker_reap.rs█worker_seq.rs█info.rs█sessions.rs█xfer.rs█visibility.rs█timefmt.rs█syncmerge.rs█subnet.rs█shellinfo.rs█seed.rs█rotation.rs█roster.rs█rename.rs█registry.rs█re [...1013ch elided...] ync.rs█notifsync.rs█netstream.rs█netbroker.rs█input_ack_deadlock.rs█idempotent.rs█dispatch.rs█daemon_e2e.rs█broker.rs█attach.rs█access.rs█## spt-msg/src/█ready.rs█deliver.rs█## xtask/src/█main.rs█## spt-runtime/src/█manifest.rs█runtime.rs█resolve.rs█lib.rs█registry.rs█profile.rs█## spt-live/src/█pulse.rs█ingest.rs█echo.rs█## spt-net/src/█net.rs█### net/█registry.rs█xfer.rs█wanmsg.rs█update.rs█sync.rs█shelllink.rs█serveprobe.rs█rest.rs█#### pairing/█ntp.rs█## spt-term/█### src/█screen.rs█winprog.rs█surface.rs█stream.rs█reader.rs█pty.rs█projection.rs█lib.rs█digest.rs█### tests/█winspawn.rs█surface.rs█stream.rs█inject.rs█dsr.rs█## spt-proto/src/█version.rs█payload.rs█lib.rs█identity.rs█id.rs█event.rs█envelope.rs█endpoint.rs█chunk.rs█addr.rs█[200 results limit reached. Use limit=400 for more]\u000f█</out>█# Tool call ¶█//Tracing daemon refresh command█grep(pattern=\"Daemon.*Refresh|daemon.*refresh|cmd_daemon|Refresh|refresh_brain|BrainRefresh|refresh\", path=\"crates\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### applyhost.rs█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█...█363|// [impl->REQ-DAEMON-REFRESH]█*364|pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {█365| let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;█366| brain█367| .request_brain_restart()█...█822|█*823| // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor█824| // brain-cycle signal as the post-swap apply - against a bare broker with NO█825| // staged release, NO release cache, NO binary swap. The verb has no staging█826| // preconditions: the signal raises and `honored` reports the supervisor█...█828| #[test]█*829| fn refresh_brain_cycles_without_staging_or_swap_preconditions() {█830| let name = unique_name( [...27450ch elided...] file_pull`/absent => copy.█*296|/// Re-adding an existing name refreshes the record and re-activates a█297|/// soft-deregistered one. Returns the record; the caller conducts█298|/// install-is-first-update through the manifest's `[update]` avenue.█299|// [impl->REQ-INSTALL-4]█...█1113| re.active && re.registered_at_ms == 2000,█*1114| \"re-add re-activates + refreshes\"█1115| );█1116| assert_eq!(registered(&adapters).len(), 1);█1117| }█### runtime.rs█618|/// shared lock and spawn against the snapshot without holding the guard (W3c,█*619|/// the live-update manifest-refresh handle).█620|#[derive(Clone)]█621|pub struct ManifestRuntime {█622| manifest: Manifest,█Showing files 1-20 of 33+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Tracing update apply command█grep(pattern=\"cmd_update|UpdateCommand|Update.*Apply|apply_update|apply_staged|ApplyHost|applyhost\", path=\"crates\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/█### src/█#### applyhost.rs█11|//!█*12|//! [`apply_staged`] drives the whole chain against what the peer pump left█13|//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):█14|//!█15|//! 1. **Re-verify at rest** (`plan_verified` under the production█...█51|█*52|/// What [`apply_staged`] did - loud + typed, never silent.█53|#[derive(Debug, Clone, PartialEq, Eq)]█54|pub enum ApplyStagedOutcome {█55| /// Nothing staged (or a torn stage that never offers) - idempotent no-op.█...█105|/// swap, carrying the verified bytes. The shared verify/classify preamble both█*106|/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on█107|/// whether a live broker is handed off to.█108|enum Prepared {█109| /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /█...█298|/// the canonical broker socket the brain handoff runs against. For a stopped box,█*299|/// the caller uses [`apply_staged_daemonless`] instead - this path REQUIRES a live█300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).█301|// [impl->REQ-UPD-3]█302|// [impl->REQ-UPD-4]█*303|pub fn apply_sta [...11306ch elided...] rivate,█...█6201| return match fetch_reject_action(class, apply) {█*6202| FetchAction::Apply => cmd_update_apply(false),█6203| FetchAction::DoneOk => {█6204| match class {█6205| FetchRejectClass::AlreadyStaged { candidate } => println!(█...█6330| if apply {█*6331| return cmd_update_apply(false);█6332| }█6333| 0█6334| }█### tests/█#### daemon_refresh_e2e.rs█4|//! harness session, and the endpoint's ONLINE perch all survive untouched - the█*5|//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI█6|//! verb. The recovery story: wedged brain-held state no longer needs a full█7|//! daemon bounce that kills every PTY.█8|//!\u000f█</out>█# Tool call ¶█//Finding brain restart supervision█grep(pattern=\"spawn_brain|restart_brain|supervis|StartReason|Refresh|BrainReady|BrainExit|handoff|refresh\", path=\"crates/spt-daemon\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## Cargo.toml█5|publish = false█*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) - the `spt` binary is a thin CLI over it. B0 lands the crate + the version...█7|█8|[dependencies]█9|# Layer-below set (R-ARCH-1 acyclic): ...->spt-live->spt-daemon->spt. The broker█## src/█### applyhost.rs█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█33|//! broker holds them all across the cycle). This replaces the old in-process█*34|//! `apply_brain_only` handoff, the regression that re-attached a CLI-side█35|//! [...49034ch elided...] te::machineid::machine_id_hash().unwrap_or_default();█651| let lease = spt_store::epoch::EpochSource::load().current();█...█827| // [unit->REQ-HAZARD-CEREMONY-CLOCK-STEP] D3 meet self-heal: on search-█*828| // deadline exhaustion the sweep forces ONE ceremony-clock refresh and tries█*829| // a final probe - proven here by a probe that FAILS until the refresh hook█*830| // fires, then succeeds. The final post-refresh sweep is what lands (the█831| // stepped-clock join heals without a daemon bounce).█832| #[tokio::test]█*833| async fn meet_retries_once_after_refresh_on_exhaustion() {█834| use std::cell::Cell;█*835| let refreshed = Cell::new(false);█[Showing lines 1-928 of 1143 (50.0KB limit). Use :929 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Finding process handle inheritance█grep(pattern=\"inherit|Stdio::|DETACHED_PROCESS|CREATE_NO_WINDOW|spawn_detached|current_exe|Command::new\", path=\"crates/spt-daemon;crates/spt\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## Cargo.toml█55|# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half█*56|# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).█57|[target.'cfg(unix)'.dependencies]█58|libc = \"0.2\"█59|█...█61|tempfile = \"3\"█*62|# Integration tests are separate crates and do not inherit the lib's deps, so the█63|# digest int-test re-declares the two it touches directly: the manifest registry█64|# (the mock adapter's `[history]` seam) and the perch-path/info resolution the█65|# on-demand projection reads.█## src/█### applyhost.rs█296|/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is█*297|/// the executable to swap (production: `std::env::current_exe()`); `broker_name`█298|/// the canonical broker socket the brain handoff runs against. For a stopped box,█299|/// the caller uses [`apply_staged_daemonless`] instead - this path REQUIRES a live█300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).█### brainproc.rs█155|///█*156|/// It inherits the broker's already-unelevated token as a plain child, so it█157|/// runs no de-elevation guard of its own.█158|// [imp [...28196ch elided...] .args([\"/PID\", &pid.to_string(), \"/T\", \"/F\"])█*322| .stdout(Stdio::null())█*323| .stderr(Stdio::null())█*324| .creation_flags(0x0800_0000) // CREATE_NO_WINDOW█325| .status();█326| }█327| #[cfg(unix)]█*328| let _ = Command::new(\"kill\")█329| .args([\"-9\", &pid.to_string()])█*330| .stdout(Stdio::null())█*331| .stderr(Stdio::null())█332| .status();█333|}█334|█### pump/█#### mod.rs█46|//!█*47|//! ## Wire discipline (inherited from the test suites, now production)█48|//!█49|//! Every pump write is fire-and-forget (`op_id: None`) - a journaled█50|//! `net_stream_send` blocks on its ack and DISCARDS interleaved events█Showing files 1-20 of 103+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Reading split architecture ADR█read(path=\"docs/adr/0018-broker-brain-process-isolation-restoration.md\", selector=null)█<out>█\u000e# Restore broker/brain process isolation (correct the in-process-collapse regression)█## Status█Accepted (2026-06-09) - **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.█## Context█ADR-0004 decided the broker/brain split **as two processes** - a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain - specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.█On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` -> `Arc<Broker>` ser [...6220ch elided...] g:** the restoration is the next milestone, before `spt-claude-code` scoping (operator-accepted 2026-06-09). Rationale: it is the *last* release that needs a manual fleet daemon bounce - paid 3× for v0.3.2 - so every adapter-era release rolls seamlessly; and the adapter is better built on the final topology than atop a daemon under later surgery while it hosts the user's daily driver. The split changes daemon internals, not the M8-frozen CLI/api surface.█- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred \"future polish\" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).█[Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading original split ADR█read(path=\"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md\", selector=null)█<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>█# Tool call ¶█//Reading coordinated update ADR█read(path=\"docs/adr/0025-live-daemon-coordinated-adapter-update.md\", selector=null)█<out>█\u000e# Live, daemon-coordinated adapter update█<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->█## Status█accepted (2026-06-22, v0.13.2) - built in W3; see the **Amendment (W3 build)** below for three corrections the build surfaced.█Extends spt's seamless-self-update objective (ADR-0004, broker/brain split) from spt-core's own brain to **adapters**.█## Context█`spt adapter update` runs **CLI-side**: it fetches, optionally verifies, and re-registers the adapter on disk (REQ-UPD-7/9 deliberately keep HTTP off the daemon). It is **daemon-blind** - it never coordinates with the running daemon. Two facts make that unsafe while an endpoint is live:█1. **A running endpoint caches the manifest in memory.** A `BrainLifecycle` clones the adapter `Manifest` at endpoint bringup (`lifecycle.rs`, the `manifest` field + the `ManifestRuntime` clone) and never re-reads it. After a disk update the in-memory manifest is **stale** - the running endpoint drives new binaries against an old manifest, or an old binary path against a new layout.█2. **The daemon runs resident adapter binaries that hold file locks.** spt-core keeps a **resident adapter binary** alive for an endpoint's lifetime - today the `[messag [...10843ch elided...] -safe-decline, infra never-kill) and clears its stale `<parent>-psyche` ready registration; it is deliberately not per-reconcile/periodic (burying the residency era, not resurrecting it).█- **A Psyche failure never touches the parent's hosted state.** Consecutive turn failures consume a bounded budget -> a `psyche_host_error` stamp on the Self perch, never a teardown of the parent endpoint (KNOWN-HAZARDS 7.30/7.31, `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` / `REQ-HAZARD-THRASH-GUARD-BLIND`).█Net: the daemon-coordinated apply is unchanged in shape - it still stops/CRC-swaps/restarts the **resident translation binary** - but the Psyche is no longer a coordination subject (nothing resident to stop), and the `.live-bin` relocation it once required is gone.█[Some lines truncated to 768 chars]\u000f█</out>█# Assistant ¶█_**Analyzing controller stream architecture**█**Tracing CLI attach connection flow**_█# Tool call ¶█//Reading CLI command definitions█read(path=\"crates/spt/src/cli.rs\", selector=\"160-290,1370-1470,3970-4090,4890-5065,5135-5210\")█<out>█\u000e Subnet {█...█action: Option<SubnetCmd>,█},█/// Task-oriented instructions for agents: `how-to <topic>`.█///█/// The binary's own usage guidance, written for an agent to read and█/// follow. Bare `how-to` lists the topics.█#[command(name = \"how-to\")]█HowTo {█/// The topic to print (omit to list available topics).█topic: Option<String>,█},█/// Endpoint operations: list, lifecycle, fork, digest, access.█///█/// The noun home for per-endpoint verbs (M8 decision 1). Bare█/// `spt endpoint` renders the merged listing - every member subnet's█/// endpoints grouped by subnet, this session's own endpoint pinned█/// distinctly at the top.█Endpoint {█#[command(subcommand)]█action: Option<EndpointCmd>,█},█/// Attach a local terminal to a broker-held endpoint PTY.█///█/// Connects to an spt-hosted session and drives it as a terminal. Local is█/// the degenerate single-node case of the cross-node attach (one pump,█/// loopback peer). Detach with the **ctrl-b** prefix then `d`█/// (`ctrl-b ctrl-b` sends a literal ctrl-b); detaching leaves the session█/// running on [...24509ch elided...] unning() && std::time::Instant::now() < down_by {█std::thread::sleep(std::time::Duration::from_millis(50));█}█if spt_daemon::is_running() {█eprintln!(█\"DAEMON_STOP_TIMEOUT: the daemon did not stop within 10s - the new version is on \\█disk; run `spt daemon start` once it stops to finish.\"█);█return 1;█}█}█match spt_daemon::start_daemon() {█Ok(_) => {█let (version, product_version, already) = match applied {█O::AppliedDaemonless {█version,█product_version,█}█| O::AppliedPending {█...█} => (version, product_version, false),█...█};█...█}█...█}█}\u000f█</out>█# Tool call ¶█//Reading refresh apply implementation█read(path=\"crates/spt-daemon/src/applyhost.rs\", selector=\"1-115,248-405,810-860,870-945\")█<out>█\u000e//! Staged-update apply orchestration (M5-D7b) - the **production caller** for█//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual█//! (\"nothing in the daemon invokes this apply after the user acks the consent█//! notif\").█//!█//! The ack IS the verb the consent notif names: its body says \"confirm to█//! apply (`spt update apply`) or dismiss\" (ADR-0007 - the harness's native█//! confirm affordance answers by running the command; dismiss latches the█//! notif and is a *prompt* answer, not a lock - an apply after a dismiss█//! still works, the user changed their mind).█//!█//! [`apply_staged`] drives the whole chain against what the peer pump left█//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):█//!█//! 1. **Re-verify at rest** (`plan_verified` under the production█//! `release-keys.json` policy): apply never trusts the cache - a tampered█//! or expired artifact refuses here. The monotonic-version floor was█//! enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time█//! re-check guards authenticity/integrity/expiry of the bytes on disk.█//! 2. **Classify** against the running broker's ABI. Only█//! [`UpdateClas [...18067ch elided...] -binary\");█std::fs::write(&exe, b\"OLD BINARY\").unwrap();█let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);█// No served_broker - deliberately. A daemonless apply must not need one.█let out = apply_staged_daemonless(&cache, &keys, &exe).expect(\"apply ok\");█assert!(█matches!(out, ApplyStagedOutcome::AppliedDaemonless { version: 7, .. }),█\"got {out:?}\"█);█assert_eq!(█std::fs::read(&exe).unwrap(),█b\"NEW BINARY BYTES\",█\"artifact landed without any broker handoff\"█);█assert_eq!(█std::fs::read(swap_aside_path(&exe, 7)).unwrap(),█b\"OLD BINARY\",█\"the outgoing binary stepped aside\"█);█...█}\u000f█</out>█# Tool call ¶█//Reading supervisor signal flow█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"1-175,175-260,680-860,860-1068\")█<out>█\u000e//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 - the█//! broker/brain process-isolation restoration, D1 skeleton).█//!█//! ADR-0004 specified the broker/brain split as two **processes**; production█//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps█//! the binary on disk but never restarts the running code (the regression█//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal█//! skeleton*: `spt daemon run` is the broker process and spawns a supervised█//! `spt daemon brain` child. The child connects over the existing broker socket█//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2█//! migrates the broker-resident loops here one per commit; **D2-1 moved the net█//! consumers** (inbound dispatcher + outbound peer pump - pure IPC clients, no█//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`█//! verb. The digest hub serve + seed-control stay broker-side continuity█//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche█//! loops.█//!█//! What D1 proves (de-risk spike #1, the design's hardest invariant): the█//! broker spawns + supervises a [...32452ch elided...] _WINDOW);█}█// Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its█// OWN process group so the daemon can reap the brain + everything it spawns█// (the shellwake watchers, the detached Psyches - all inherit this group) as█// one unit on stop, without signalling the daemon itself. Windows roots the█// reap on a Job object instead (assigned in the supervisor via BrainReaper).█#[cfg(unix)]█{█use std::os::unix::process::CommandExt;█unsafe {█cmd.pre_exec(|| {█// setpgid(0, 0): this child becomes the leader of a new group█// (pgid == its pid). Best-effort - a failure only loses the reap.█libc::setpgid(0, 0);█Ok(())█});█}█}█...█}\u000f█</out>█# Tool call ¶█//Reading daemon startup boundaries█read(path=\"crates/spt-daemon/src/daemon.rs\", selector=\"1-125,170-395,480-715,715-890\")█<out>█\u000e//! The daemon process entrypoint + auto-start (B6).█//!█//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds█//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the█//! canonical per-home names ([`crate::endpoint`]) and serves both for the life█//! of the process.█//!█//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`█//! invocation calls it; if no daemon answers the seed-control ping it spawns█//! `spt daemon` detached and waits (bounded) for it to come up. The reliable█//! anchor is the seed channel - bound for the whole daemon lifetime, so a ping█//! against it is a true liveness test (`listen` is the interim model's anchor;█//! here the seed channel plays that role for cold-start detection).█// [impl->REQ-DAEMON-3]█use std::io;█#[cfg(unix)]█use std::process::{Command, Stdio};█use std::sync::Arc;█use std::thread;█use std::time::Duration;█use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};█use crate::broker::{default_journal_path, Broker};█use crate::config::{DaemonConfig, RelayChoice};█use crate::digesthub::{self, DigestHub};█use crate::endpoint::{█broker_socket_name, daemon_pid_path [...35122ch elided...] le, not the job). A job-neutral creator (WMI / Task█/// Scheduler) makes the daemon a child of WmiPrvSE / the scheduler instead -█/// outside any terminal job from birth, which is why Task-Scheduler-autostarted█/// daemons never had this bug. Breakaway is demoted to a fallback because a job█/// CAN deny it (the L1 finding).█// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]█#[cfg(windows)]█#[derive(Debug, Clone, Copy, PartialEq, Eq)]█pub(crate) enum LaunchRung {█/// WMI `Win32_Process.Create` via absolute powershell - daemon = WmiPrvSE's█/// child, job-escaped regardless of the terminal job's breakaway policy.█Wmi,█/// `schtasks` one-shot - Task-Scheduler-owned, job-neutral.█Schtasks,█/// `CREATE_BREAKAWAY_FROM_JOB` - escapes only where the job permits it.█Breakaway,█...█}\u000f█</out>█# Tool call ¶█//Locating restart broker dispatch█grep(pattern=\"KIND_BRAIN_RESTART|set_brain_restart|request_brain_restart|BrainRestarted|brain-restarted\", path=\"crates/spt-daemon/src\", 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,█...█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 daemon bounce. Nothing is verified, staged, or renamed: the█...█366| brain█*367| .request_brain_restart()█368| .map_err(|e| format!(\"brain-restart trigger: { [...7933ch elided...] ), so the caller knows the trigger█237|/// reached a broker that cannot itself respawn.█*238|pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";█239|█240|/// `spawn` payload - the program + args + initial geometry. The broker spawns█241|/// it under a PTY (no shell interpretation; argv is passed through).█...█871|█*872|/// `brain-restarted` payload - the broker's ack for [`KIND_BRAIN_RESTART`]█873|/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to█874|/// cycle the child; false when the broker has none wired (a bare test broker),█875|/// so the caller can tell a real planned restart from a mere accept.█876|#[derive(Debug, Clone, Serialize, Deserialize)]█*877|pub struct BrainRestarted {█878| #[serde(default)]█879| pub honored: bool,█880|}\u000f█</out>█# Tool call ¶█//Locating controller connection ownership█grep(pattern=\"Role::Controller|Role::Viewer|ControllerSink|controllers|viewers|serve_attach|KIND_ATTACH|attach\", path=\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/attach.rs;crates/spt/src/rc.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### broker.rs█13|//! crash). The broker - and every hosted child - must survive that untouched,█*14|//! and the new brain must re-attach to the output stream **gaplessly and█15|//! exactly-once** (Spike #1 made real). Two mechanisms:█16|//!█17|//! - **Sequenced, buffered output.** Each session's output is appended to an█...█24|//! live. The brain dedups by `seq`. Output produced *while no brain was█*25|//! attached* sits in the ring and is replayed on the next subscribe - so a█26|//! brain restart loses nothing.█27|//!█28|//! Sessions are therefore **broker-owned and outlive any one connection**: a█...█44|█*45|use spt_net::net::attach::AttachIntent;█46|use spt_net::net::endpoint::EndpointAddr;█47|use spt_store::perch::{resolve_perch_path, ParentHint};█48|█...█93|/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely█*94|/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread█*95|/// owns the replay batch), so a viewer attaching to a busy session is not█96|/// spuriously evicted.█97|const VIEWER_CHANNEL_DEPTH: usize = 256;█98|█*99|/// Soft cap on concurrent viewers per session (REQ- [...14310ch elided...] the broker sessions map (ADR-0025 W3a) - independent of█*875|/// perch status, so an UNBOUND skeleton's live session reports attachable.█876|///█877|/// [`has_session`]: SessionProbe::has_session█878|pub(crate) struct SessionProbe {█...█1081|// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]█*1082|pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {█*1083| run_attach_inner(endpoint_id, intent, false)█1084|}█1085|█*1086|/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker█1087|/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via█1088|/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a█1089|/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\u000f█</out>█# Tool call ¶█//Reading refresh end-to-end test█read(path=\"crates/spt/tests/daemon_refresh_e2e.rs\", selector=null)█<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 ¶█//Finding lifecycle focused tests█grep(pattern=\"refresh|restart|survive|controller|viewer|brain.*restart|attach.*restart|update\", path=\"crates/spt-daemon/tests;crates/spt/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/tests/█## access.rs█146|// unsolicited traffic again - store edits take effect mid-stream, no daemon█*147|// restart.█148|#[test]█149|fn whitelist_refuses_exempts_replies_and_admits_listed() {█150| init_home();█...█220| // (3) Whitelist node A; its unsolicited traffic now passes - the gate█*221| // re-read the store mid-stream (no restart, no resubscribe).█222| let mut store = AccessStore::load();█223| store.allow(&target, &a_node);█224| store.save().expect(\"save access store\");█## attach.rs█13|//! on the stream table's handshake-proven `remote_id_hex`;█*14|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies█15|//! mid-attach; the broker keeps the QUIC stream, the session, the output█16|//! log, and the effect journal. The successor brain re-serves the SAME█17|//! stream from seq 0 - the worst-case stale cursor: the replayed input█...█27|use spt_daemon::attach::{█*28| read_controller_event_resuming, request_attach, send_attach_input, serve_attach,█*29| AttachServeOutcome, ControllerIrrecoverablyBehind,█30|};█31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.█32 [...48803ch elided...] ssert!(█*271| broker.test_seed_wedged_local_controller(sid, 1_000),█272| \"precondition: the wedge seed must install on the hosted session\"█273| );█274| drop(a);█...█278| assert!(█*279| broker.any_local_controller_wedged(),█*280| \"precondition: the old-gen local controller conn must read WEDGED before the trial\"█281| );█282|█283| // -- Drive the REAL promotion path (supervise_brain -> run_trial) with a scripted█## handoff.rs█1|//! B2 regression - the milestone's load-bearing proof: a brain can be killed and█*2|//! restarted repeatedly while the broker and its hosted child survive untouched,█3|//! and each new brain re-attaches to the output stream **gaplessly** with a fresh█[Showing lines 1-956 of 1197 (49.9KB limit). Use :957 to continue]\u000f█</out>█# Assistant ¶█_**Identifying relevant documentation update**_█# Tool call ¶█//Reading update wedge code audit█read(path=\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\", selector=null)█<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 update wedge rig verdict█read(path=\"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\", selector=null)█<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 ¶█//Finding wedge lifecycle reports█glob(path=\"docs/**/*WEDGE*.md; docs/**/*BRAIN*.md; docs/**/*LIFECYCLE*.md\", hidden=true, gitignore=true, limit=200)█<out>█\u000e# docs/█UPDATE-WEDGE-2-ROUND3-CODEREAD.md█UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md█NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md█BROKER-BRAIN-SPLIT-RESTORATION.md\u000f█</out>█# Tool call ¶█//Reading controller output architecture█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"1-190,480-620,690-880,1450-1650,2700-2730,3080-3220,3840-3910\")█<out>█\u000e//! The broker kernel (B1 + B2) - the stable half of the broker/brain split.█//!█//! Per ADR-0004 §B the broker holds **only** the resources a live consumer█//! would lose continuity on if the logic restarted: the PTY master+writer and█//! the spawned child (both via [`spt_term::PtySession`]), and the accepted█//! client / listening sockets. It runs **no logic** - no routing, registry,█//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,█//! forward its output to the brain, inject the brain's input, report exit.█//!█//! ## Gapless handoff (B2 - the load-bearing invariant)█//!█//! A brain may be killed and restarted at any moment (a routine self-update, a█//! crash). The broker - and every hosted child - must survive that untouched,█//! and the new brain must re-attach to the output stream **gaplessly and█//! exactly-once** (Spike #1 made real). Two mechanisms:█//!█//! - **Sequenced, buffered output.** Each session's output is appended to an█//! [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held█//! in a bounded ring. The current subscriber (a brain connection) receives█//! chunks live; all subscriber writes happen *under the log l [...50318ch elided...] rted { honored })█.expect(\"BrainRestarted serializes\"),█);█send_frame(send, &frame);█}█/// Dial a peer over the broker-owned endpoint (D4a).█///█/// With an `op_id` the dial is routed through the broker-owned█/// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,█/// exactly like an idempotent PTY write: a replay of the same id after a█/// brain crash is deduped (no second connection) and answered with the█/// connection the first apply opened. The `conn_id` is recorded *inside*█/// the journaled closure, so a concurrent deduped replay always finds it.█/// A deduped id whose connection this process no longer holds (a broker█/// restart - the journal survives, connections don't) is a typed error\u000f█</out>█# Tool call ¶█//Reading attach service core█read(path=\"crates/spt-daemon/src/attach.rs\", selector=\"1-205,205-455\")█<out>█\u000e//! Remote terminal attach (M4-D5b, REQ-INST-8) - serve a byte-stream viewport█//! onto a local session for an operator on another node.█//!█//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent█//! [`AttachRecord::Request`]; this module is the target-side pump between that█//! stream and the local broker-hosted session:█//!█//! - session `Output` chunks -> [`AttachRecord::Output`] lines onto the wire█//! (fire-and-forget sends; `Output.seq` carries the dedup authority);█//! - inbound [`AttachRecord::Input`] -> [`Brain::send_effect`] with the█//! operator-minted `op_id` - exactly-once at the PTY write through the B5█//! journal (a replayed input record cannot double-type);█//! - session `Exit` -> [`AttachRecord::Exit`] + stream finish.█//!█//! ## Exactly-once lives at the EDGES, not the wire sends█//!█//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a█//! journaled [`Brain::net_stream_send`] blocks reading events until its ack█//! and *discards* everything interleaved - inside a full-duplex pump loop that█//! eats the operator's `Input` records (both sides starve; found the hard way█//! in `tests/attach.rs`). Idempotency holds with [...23615ch elided...] e endpoint, never█// keeps it awake (Gap B). Discard any racing input.█pending_input.clear();█}█O::BusyControlled { .. } => {█// A different operator controls it - refuse (no PTY input█// ever reached it; the buffer is discarded). The rc█// renders the busy guidance client-side from driven_by.█wire.net_stream_send(stream_id, &[], None, true)?;█return Ok(AttachServeOutcome::Refused);█}█}█}█// Viewer letterbox feed: the current PTY size (on attach + resize).█...█}█}█}█[Showing lines 1-458 of 627. Use :459 to continue]\u000f█</out>█# Tool call ¶█//Reading rc pump lifecycle█read(path=\"crates/spt/src/rc.rs\", selector=\"1-45,850-950,1080-1280,1280-1470\")█<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 [...24084ch elided...] lingo).█let mut established: Option<u64> = Some(conn_id);█let stream_id = spt_daemon::with_tracing_retry(█// Operator attach open - the rc OpMinter is the counter (ADR-0034 `rc` tag).█|| Ok(MintedOp::new(Minter::Rc, ops.next())),█|op| {█let cid = match established.take() {█Some(c) => c,█None => match local_session {█// Local: the broker's in-process loopback singleton (re-mint/reuse).█Some(_) => brain█.net_dial_loopback()█.map_err(|e| {█...█})?█.conn_id,█...█},█};█...█},█)█.map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;█...█}\u000f█</out>█# Tool call ¶█//Reading broker connection dispatch█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"2910-3225,3760-3910\")█<out>█\u000e█/// The child pid of a hosted session, if present (tests assert it is stable█/// across brain restarts).█pub fn session_pid(&self, id: u64) -> Option<u32> {█self.sessions█.lock()█.unwrap()█.get(&id)█.and_then(|h| h.session.process_id())█}█/// Broker-truth read for the brain promotion DRAINED gate█/// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the█/// brain's own `by: None` conn) still WEDGED - blocked in a socket write past the█/// brain-write deadline? The brain supervisor (in THIS broker process - no round-█/// trip) promotes a ready update candidate only once this reads `false`: an OLD█/// generation whose local controller conn is black-holed reads wedged until the W2█/// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer█/// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-█/// closed old conn is simply absent -> `false` at once (the normal fast promote).█/// Follows the established sessions->log lock order (KIND_SESSIONS reap); reads█/// only `write_blocked_since` under each lo [...20737ch elided...] rted { honored })█.expect(\"BrainRestarted serializes\"),█);█send_frame(send, &frame);█}█/// Dial a peer over the broker-owned endpoint (D4a).█///█/// With an `op_id` the dial is routed through the broker-owned█/// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,█/// exactly like an idempotent PTY write: a replay of the same id after a█/// brain crash is deduped (no second connection) and answered with the█/// connection the first apply opened. The `conn_id` is recorded *inside*█/// the journaled closure, so a concurrent deduped replay always finds it.█/// A deduped id whose connection this process no longer holds (a broker█/// restart - the journal survives, connections don't) is a typed error\u000f█</out>█# Tool call ¶█//Reading brain connection methods█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"320-420,600-770,1220-1480\")█<out>█\u000e pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {█let mut conn = LocalSocketTransport::connect(name)?;█send_hello(&mut conn, Role::Brain)?;█Ok(Brain {█conn: BrainConn::Whole(conn),█generation: 0,█gen_start_ms: now_ms,█session_id: None,█next_seq: 0,█net_cursors: std::collections::HashMap::new(),█presence_cursor: None,█session_cursors: std::collections::HashMap::new(),█io_timeout: None,█baseline_next_output: false,█})█}█/// Cold start in **PUMP mode**: connect, handshake, then SPLIT the stream█/// into the reader-thread carrier ([`BrainConn::Split`]) and arm a per-IPC-█/// call deadline of `io_timeout`. This is the peer pump's dedicated client -█/// every reply read is bounded ([`Brain::read_event_until`] via█/// [`Brain::call_deadline`]) so a black-holed peer can never wedge the█/// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a█/// live `Whole` after the fact - moving the `Stream` out by value to split it█/// is impossible behind `&mut self`), so a pu [...24093ch elided...] Data, e))?;█return Ok(reply.honored);█}█BrokerEvent::Error { message } => return Err(io::Error::other(message)),█_ => continue,█}█}█}█/// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT█/// ID (REQ-SEND-SPT-HOSTED) - the endpoint-keyed inject a CLI `spt send` uses█/// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The█/// broker resolves endpoint->session atomically and writes the bytes (a█/// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:█/// `delivered=true` = broker injected via translation binary; `spool_deferred=true`█/// = caller should spool deferred (active window), `false` = spool non-deferred\u000f█</out>█# Assistant ¶█_**Assessing update stall causes**_█# Tool call ¶█//Finding wedge hazard contracts█grep(pattern=\"UPDATE-WEDGE|BRAIN-RESUME-NO-CONN-DEADLOCK|BROKER-VIEWER-BRAIN-DECOUPLE|SHAREDSEND-NO-BLOCKING|UPDATE-PROMOTE-DRAINED|update wedge|brain conn\", path=\"docs/KNOWN-HAZARDS.md;docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md;CHANGELOG.md;CONTEXT.md\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# CHANGELOG.md█194|█*195|A reliability patch closing the last update-wedge failure mode: a stuck session█196|consumer can no longer freeze the daemon under load.█197|█198|> **Applying this update needs a daemon restart.** Unlike the recent seamless█# docs/█## KNOWN-HAZARDS.md█623|█*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]`█*625|<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->█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...█*627|- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT- [...3685ch elided...] ight: the v0.27->v0.28 update wedge,█4|the mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake█5|double-spawn. Every root below is PINNED (rig-reproduced or line-level) - evidence ledger in█6|doyle's findings backlog (seeds #5-#10) and this doc's per-wave notes. Owner: doyle (gate),█...█48|█*49|## W2 - broker viewer/brain decouple (flagship - the update wedge)█50|█*51|**REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE** (doc, impl, unit, int)█52|Root, rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): PTY viewer fan-out█53|and control mutations depend synchronously on a live, DRAINING brain - brain-subscriber█54|session-output writes ride UNDER the per-session log lock (broker.rs:19-20). Failed writes are█[Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Finding current resume implementation█grep(pattern=\"resume_session_cursors|resume_sessions|any_local_controller_wedged|old_gen_drained|write_ready|net_status\\\\(\\\\)|sessions\\\\(\\\\)\", path=\"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/false_promote.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## src/█### brain.rs█76|/// respawns reconstructs continuity by querying the broker█*77|/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this█78|/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`█79|/// solely because the integration tests (separate test target - `#[cfg(test)]`█80|/// would hide them) still drive the handoff-frame shape directly; there is **no█...█282| /// continuity state a cold-started brain rebuilds from the broker via█*283| /// [`Brain::resume_sessions`], replacing the retired single-session█284| /// `BrainState` handoff. A session present here is in **resume mode** - its█285| /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded█286| /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once█...█380| /// **Test-only (restoration D4-3).** The production resume path is█*381| /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),█382| /// never this brain->brain frame; `handoff` survives only for the integration█383| /// tests. See [`BrainState`].█384| // [impl->REQ-HAZARD-CONTROLL [...16299ch elided...] n AND raises `stop` so `supervise_brain` returns.█192|struct FalsePromoteEnv {█...█229| // The gate under test: REAL broker truth, no brain round-trip.█*230| fn old_gen_drained(&self) -> bool {█*231| !self.broker.any_local_controller_wedged()█232| }█233|}█234|█...█278| assert!(█*279| broker.any_local_controller_wedged(),█280| \"precondition: the old-gen local controller conn must read WEDGED before the trial\"█281| );█282|█...█333| // -- Drain the old-gen conn: a second operator's take stall-evicts the wedged local█*334| // incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. --█335| remote_take(&name, sid);█336|█337| // Poll for the promotion the drain unblocks (bounded - the gate flips within a tick\u000f█</out>█# Tool call ¶█//Finding connection poison implementation█grep(pattern=\"struct BrokerConn|write_gated|write\\\\(|poison|deadline|retire|cancel\", path=\"crates/spt-daemon/src/conn.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e*1|//! Bounded, cancelable, \u000epoison-on-failure framed writes for one physical█2|//! broker connection (UPDATE-WEDGE round-4).█3|//!█4|//! ## The failure class this kills█...█12|//! logical stall-evict (`stall_evict_controller`) releases only the controller█*13|//! ROLE - it never cancels the in-flight pipe write or closes the physical█14|//! connection, so the writer (and every write queued behind the same gate)█15|//! stays wedged until the peer process dies (~128 s in the field capture, on a█16|//! brain restart).█...█23|//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE█*24|//! deadline stamped at write entry covers BOTH the gate-wait and the OS█25|//! write);█26|//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute█*27|//! deadline, then performs the blocking [`write_frame`] OUT of every lock;█28|//! 3. on success, returns the half to the gate.█29|//!█*30|//! The watchdog fires OUT OF BAND at the deadline - it never relies on the█*31|//! write returning, nor on opportunistic stall-evict sampling: it poisons the█32|//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows - the█33|//! completion APC [...19021ch elided...] 68| organic = Some(e);█769| break;█...█777| io::ErrorKind::TimedOut,█*778| \"an organic failure must NOT surface as the deadline class\"█779| );█*780| // Poisoned + retired: the next write refuses immediately.█*781| let refused = conn.write(&env).expect_err(\"poisoned conn refuses writes\");█782| assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);█783| }█784|█...█808| // The id (and the capped label) stay stable across records.█*809| let one = a.inner.render_retirement(true, None);█*810| let two = a.inner.render_retirement(true, None);█811| assert!(one.contains(&format!(\"conn={}\", a.id())));█812| assert!(two.contains(&format!(\"conn={}\", a.id())));█813| }\u000f█</out>█# Tool call ¶█//Reading lifecycle truth triage█read(path=\"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\", selector=null)█<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 [...12716ch elided...] -publish drift gate applies.█---█## Sequencing + gate notes█- W1 ships alone and fast (three small REQs, one seam) - unblocks two live psyches; doyle█field-accepts on this node same-day.█- W2 needs a doyle design ruling on the decouple shape BEFORE impl (todlando proposes; the█log-lock/subscriber contract is KNOWN-HAZARDS-grade). Its int test IS tonight's rig.█- W3 composes: FINISH rides ENDPOINT-SURVIVAL. RC-RECONNECT-TRUTH is separable if the wave█runs long.█- W4/W5 independent of W2/W3 - parallelizable if a second executor frees up, else sequential.█- Every wave: traceable activate -> build -> `traceable-reqs check` exit-0 -> doyle gate█(worktree of the wave sha) -> next wave. Release cut at milestone end per RELEASE-RUNBOOK█(bump-ON-milestone-branch pattern, deployah drives).█\u000f█</out>█# Tool call ¶█//Reading current brain heartbeat█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"155-335,1080-1145\")█<out>█\u000e///█/// It inherits the broker's already-unelevated token as a plain child, so it█/// runs no de-elevation guard of its own.█// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {█// Persist this brain generation's stderr to the rotating SPT_HOME/logs sink█// (REQ-DAEMON-STDERR-PERSIST) - a detached brain's stdio is /dev/null, so every█// diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs█// FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.█// [impl->REQ-DAEMON-STDERR-PERSIST]█let _ = crate::stderrlog::install(\"brain\", generation);█let name = broker_socket_name();█let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;█// The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the█// 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█// con [...11789ch elided...] let child =█spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;█// Enroll the (re)spawned brain into the reap container so a█// `daemon stop` tears down the brain + its Psyches as one unit█// (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]█reaper.enroll(&child);█Ok(child)█},█);█});█(handle, restart)█}█#[cfg(test)]█mod tests {█use super::*;█use std::sync::atomic::AtomicU32;█/// A trivial, fast-exiting real child - the cross-OS no-op the supervisor█/// test respawns (stands in for the brain process).█fn fast_child() -> io::Result<Child> {█#[cfg(windows)]█{█...█}█...█}█...█}\u000f█</out>█# Tool call ¶█//Reading current resume methods█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"430-585\")█<out>█\u000e pub fn session_id(&self) -> Option<u64> {█...█}█/// Snapshot durable state for a future handoff. **Test-only (restoration█/// D4-3)** - the production path reconstructs from the broker, never a█/// snapshot frame; see [`BrainState`].█pub fn snapshot(&self) -> BrainState {█BrainState {█session_id: self.session_id.expect(\"snapshot before a session exists\"),█generation: self.generation,█next_seq: self.next_seq,█gen_start_ms: self.gen_start_ms,█}█}█/// Spawn a session under a broker PTY; the broker auto-subscribes this█/// connection. Reads until the `spawned` reply (consuming any output that█/// races ahead).█pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {█self.spawn_session_pid(req)█.map(|(session_id, _)| session_id)█}█/// [`spawn_session`](Self::spawn_session) that also surfaces the child's█/// OS pid (when the backend exposes one) - measurement/observability█/// callers (the D9-3 dormancy-budget harness) sample the seat by pid.█///█/// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below█[...5680ch elided...] d(█KIND_KILL,█serde_json::to_value(KillReq { session_id: sid }).expect(\"KillReq serializes\"),█)█}█/// Read output events until the accumulated bytes contain `needle`,█/// appending all received output to `buf`. Returns the highest seq seen.█/// A session `Exit` before the needle is found is an error (the child died).█pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {█loop {█if contains(buf, needle) {█return Ok(self.next_seq.saturating_sub(1));█}█match self.read_event()? {█BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),█...█}█}█}█[Showing lines 429-588 of 2178. Use :589 to continue]\u000f█</out>█# Tool call ¶█//Reading bounded connection writes█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"1-330\")█<out>█\u000e//! Bounded, cancelable, poison-on-failure framed writes for one physical█//! broker connection (UPDATE-WEDGE round-4).█//!█//! ## The failure class this kills█//!█//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard█//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes█//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` - a local-socket█//! path with **no supported write timeout** (`set_timeout` -> `Unsupported`).█//! A connected consumer that stops reading (an `rc --take` whose process█//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the█//! logical stall-evict (`stall_evict_controller`) releases only the controller█//! ROLE - it never cancels the in-flight pipe write or closes the physical█//! connection, so the writer (and every write queued behind the same gate)█//! stays wedged until the peer process dies (~128 s in the field capture, on a█//! brain restart).█//!█//! ## The primitive█//!█//! [`BrokerConn`] owns the send half outright - it never leaves this module,█//! so an unbounded bypass write cannot compile. Every write:█//!█//! 1. acquires the serialized write GATE with a bounded wait (an [...13848ch elided...] t slipped-in read. On Unix the█/// `shutdown` in [`poison_and_cancel`] already did all of this.█// On Unix the cfg(windows) teardown strips out and the idempotence█// guard's early `return` becomes the function tail - needless_return█// there, load-bearing here. Restructuring (e.g. `if !swapped { ... }`)█// would leave Unix an empty if-body (`clippy::needless_if`) instead.█#[cfg_attr(unix, allow(clippy::needless_return))]█fn sever(&self) {█if self.severed.swap(true, Ordering::AcqRel) {█return;█}█#[cfg(windows)]█// SAFETY: as in poison_and_cancel - handle outlives the watchdog.█unsafe {█let _ = win::DisconnectNamedPipe(self.raw);█...█}█}█...█}█[Showing lines 1-333 of 815. Use :334 to continue]\u000f█</out>█# Tool call ¶█//Reading resume deadlock test█read(path=\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\", selector=\"1-360\")█<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 ¶█//Reading false promotion test█read(path=\"crates/spt-daemon/tests/false_promote.rs\", selector=\"1-340\")█<out>█\u000e//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) - the FALSE-PROMOTE int rig, on a█//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the█//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` -> `run_trial`), the gate█//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.█//!█//! ## The bug this gate locks down (the 22:47 false-promote)█//!█//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-█//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) - it does NOT close it. A new brain█//! can signal `brain.ready` INSIDE that window while the OLD generation's local█//! controller conn is still wedged (blocked writes pending), so `update apply`█//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The█//! fix: the promotion gate adds a DRAINED precondition - promote only on ready AND█//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),█//! read from BROKER TRUTH in-process (no brain round-trip).█//!█//! ## The wedge analog (round-4 revision - statically held, doyle Option C)█//!█//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside█[...12215ch elided...] e OLD generation's conn is still wedged\"█);█assert!(█env.rollbacks.lock().unwrap().is_empty(),█\"and it must not have rolled back yet - the window is far from elapsed\"█);█// -- Drain the old-gen conn: a second operator's take stall-evicts the wedged local█// incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. --█remote_take(&name, sid);█// Poll for the promotion the drain unblocks (bounded - the gate flips within a tick█// of the evict; a stall-evict is prompt, not a hang).█let deadline = Instant::now() + Duration::from_secs(8);█let promoted = loop {█if !env.promotions.lock().unwrap().is_empty() {█break true;█}█...█};█...█}█[Showing lines 1-343 of 376. Use :344 to continue]\u000f█</out>","textHead":"# User ¶█Complete the assignment below, thoroughly:█# Target█Daemon refresh/update implementation and process-lifecycle boundaries; exact symbols and call chain. Non-goal: general update review.█# Change█Trace `spt daemon refresh` and `spt update` through CLI, service manager, daemon brain, broker, inherited handles, and restart/refresh behavior. Identify operations capable of invalidating or pausing existing PTY controller streams while leaving broker PTYs alive. Rank falsifiable candidates with evidence.█# Acceptance█Report exact line-anchored symbols, process transitions, likely race windows, and existing focused tests or missing test seams. No writes or commands that alter runtime state.█# Assistant ¶█_**Planning skill and doc reads**_█# 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 lifecycle sources█glob(path=\"**/*.{ts,tsx,js,mjs,cjs,md,json}\", hidden=true, gitignore=true, limit=500)█<out>█\u000eCHANGELOG.md█CONTEXT.md█README.md█CLAUDE.md█V0.19.0-P6-SCREEN-GRID-DESIGN.md█V0.17.0-ROBUST-JOIN-AND-PRESENCE-JIT.md█V0.16.0-UPDATE-ARC-AND-CLI-JIT.md█AGENTS.md█V0.15.0-PLAN.md█ROADMAP.md█F-019-TRANSLATION-BINARY-INSTALL-DIR-RESOLVE-DESIGN.md█V0.14.1-HOME-SUBNET-PICKER-LAYER-DESIGN.md█F-018-ADAPTER-ADD-NONDESTRUCTIVE-DESIGN.md█V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md█W3-DESIGN-PROPOSAL.md█W3-GATE-VERDICT.md█W3-DESIGN-GATE.md█V0.13.2-ADAPTER-PACKAGING-JIT.md█V0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md█V0.13.0-W4-PICKER-UX-PLAN.md█V0.13.0-VIEWER-RING-ROLL-SNAP-JIT.md█V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md█V0.13.0-SKIP-TO-LIVE-JIT.md█V0.13.0-RC-VT-TRANSLATE-DESIGN.md█V0.13.0-P2-SESSION-RESUME-DESIGN.md█V0.13.0-P1C-CONTROLLER-WRITER-REORDER-PLAN.md█V0.13.0-P1-RC-PASTE-DESIGN.md█V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md█V0.13.0-P0-JIT-PLAN.md█V0.13.0-DELIVERY-CONTROL-JIT.md█V0.13.0-BUG2-RC-VT-TRANSLATE-PLAN.md█V0.12.1-LIFECYCLE-JIT.md█V074-PLAN.md█M11-PLAN.md█DAEMON-LIFECYCLE-COMPLETION-PLAN.md█REQ-MSG-ENVELOPE-PLAN.md█PRD.md█M12-W4-RULING.md█M12-W5-PLAN.md█M12-W4-PLAN.md█M12-W3-PLAN.md█M12-W3-RULING.md█M12-W2-RULING.md█M12-W2.5-PLAN.md█M12-W2.5-RULING.md█M12-W2-PLAN.md█M12-W1-PLAN.md█M12-W1.5-PLAN.m [...3628ch elided...] -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█## design/█subnet-presence-display.md█robust-wan-subnet-join.md█v0.16.0-update-arc-and-cli.md█## spikes/█SPIKE-06-idempotent-boundary.md█SPIKE-07-branchstore.md█SPIKE-04-forkpty-parity.md█SPIKE-05-restart-stress.md█SPIKE-03-quic-survival.md█# crates/spt-runtime/█manifest.schema.json█# releases-repo/█SYNC.md█README.md█[200 results limit reached. Use limit=400 for more]\u000f█</out>█# Tool call ¶█//Locating refresh update paths█grep(pattern=\"daemon refresh|\\\\brefresh\\\\b|\\\\bupdate\\\\b|restart|broker|brain\", path=\"src;packages;test;tests;docs;CONTEXT.md\", 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- [...48705ch elided...] . 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-...█14|█15|## Both legs GREEN█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.█## KNOWN-HAZARDS.md█4|█[Showing lines 1-364 of 725 (49.8KB limit). Use :365 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Reading domain context█read(path=\"CONTEXT.md\", selector=null)█<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**: [...58438ch 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-300 of 883. Use :301 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading known hazards█read(path=\"docs/KNOWN-HAZARDS.md\", selector=null)█<out>█\u000e# Known Hazards█Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** - the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.█**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon<->broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.█---█## 1. Race conditions & ordering█### 1.1 Phantom INIT_SIGNOFF after grace period█- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spoole [...51630ch elided...] structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.█- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.█### 6.2 Soft-cleanup preserves state, removes `ready`█- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.█- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.█- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.█- **Sister cite:** `src/owl/stop.rs`.█...█...█<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->█[Showing lines 1-300 of 736. Use :301 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading rollout guidance█read(path=\"docs/DEBUG-ROLLOUT.md\", selector=null)█<out>█\u000e# Debug rollout runbook█<!-- [doc->REQ-UPD-6] -->█Debug rollout is the maintainer-only fast path for testing a local spt-core█build across a trusted lab subnet. It uses the normal signed self-update█substrate: no raw peer file-copy, no production `spt` CLI surface, and no█embedded debug trust anchor.█Use this when debugging needs a local build to reach multiple lab nodes quickly.█Do not use it for public releases; use `docs/RELEASE-RUNBOOK.md` for that.█## Mental model█- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one█artifact digest per Rust target triple.█- Each recipient verifies the set under its node-local `release-keys.json`,█selects only its own platform artifact, stages it, and then follows the normal█consent/apply policy.█- Debug and stable are separate channels. A debug-pinned node accepts only█`channel = \"debug\"` offers; a stable-pinned node accepts only stable offers.█- Debug versions are monotonic within the debug channel. To recover from a bad█debug build, publish a higher debug version, even if the bytes are a previous█known-good binary.█- A broker-touching candidate may be staged, but apply refuses it while█broker-hel [...2707ch elided...] t triple.█- `WrongChannel` means the node is not pinned to `debug`.█- `Rollback` means the offered debug version is not greater than the node's█current debug-channel version.█- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held█resources; quiesce hosted endpoints first.█## Agent checklist█1. Read `CONTEXT.md` \"debug rollout\" and ADR-0016 before changing the flow.█2. Keep debug tooling in `xtask` or other maintainer tooling, not the public█`spt` CLI/help surface.█3. Keep verification on the production update substrate: signed metadata,█per-platform artifact digest, channel pinning, monotonic version, and█apply-time re-verification.█4. Add or update traceability evidence for `REQ-UPD-6`.█5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.█\u000f█</out>█# Tool call ¶█//Finding architecture decisions█glob(path=\"docs/adr/**/*.md\", hidden=true, gitignore=true, limit=200)█<out>█\u000e# docs/adr/█0037-materialized-project-index.md█0036-private-fork-distribution.md█0005-totp-seeded-spake2-node-pairing.md█0031-server-side-screen-grid-ren","textTail":"\u000epoison-on-failure framed writes for one physical█2|//! broker connection (UPDATE-WEDGE round-4).█3|//!█4|//! ## The failure class this kills█...█12|//! logical stall-evict (`stall_evict_controller`) releases only the controller█*13|//! ROLE - it never cancels the in-flight pipe write or closes the physical█14|//! connection, so the writer (and every write queued behind the same gate)█15|//! stays wedged until the peer process dies (~128 s in the field capture, on a█16|//! brain restart).█...█23|//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE█*24|//! deadline stamped at write entry covers BOTH the gate-wait and the OS█25|//! write);█26|//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute█*27|//! deadline, then performs the blocking [`write_frame`] OUT of every lock;█28|//! 3. on success, returns the half to the gate.█29|//!█*30|//! The watchdog fires OUT OF BAND at the deadline - it never relies on the█*31|//! write returning, nor on opportunistic stall-evict sampling: it poisons the█32|//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows - the█33|//! completion APC [...19021ch elided...] 68| organic = Some(e);█769| break;█...█777| io::ErrorKind::TimedOut,█*778| \"an organic failure must NOT surface as the deadline class\"█779| );█*780| // Poisoned + retired: the next write refuses immediately.█*781| let refused = conn.write(&env).expect_err(\"poisoned conn refuses writes\");█782| assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);█783| }█784|█...█808| // The id (and the capped label) stay stable across records.█*809| let one = a.inner.render_retirement(true, None);█*810| let two = a.inner.render_retirement(true, None);█811| assert!(one.contains(&format!(\"conn={}\", a.id())));█812| assert!(two.contains(&format!(\"conn={}\", a.id())));█813| }\u000f█</out>█# Tool call ¶█//Reading lifecycle truth triage█read(path=\"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\", selector=null)█<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 [...12716ch elided...] -publish drift gate applies.█---█## Sequencing + gate notes█- W1 ships alone and fast (three small REQs, one seam) - unblocks two live psyches; doyle█field-accepts on this node same-day.█- W2 needs a doyle design ruling on the decouple shape BEFORE impl (todlando proposes; the█log-lock/subscriber contract is KNOWN-HAZARDS-grade). Its int test IS tonight's rig.█- W3 composes: FINISH rides ENDPOINT-SURVIVAL. RC-RECONNECT-TRUTH is separable if the wave█runs long.█- W4/W5 independent of W2/W3 - parallelizable if a second executor frees up, else sequential.█- Every wave: traceable activate -> build -> `traceable-reqs check` exit-0 -> doyle gate█(worktree of the wave sha) -> next wave. Release cut at milestone end per RELEASE-RUNBOOK█(bump-ON-milestone-branch pattern, deployah drives).█\u000f█</out>█# Tool call ¶█//Reading current brain heartbeat█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"155-335,1080-1145\")█<out>█\u000e///█/// It inherits the broker's already-unelevated token as a plain child, so it█/// runs no de-elevation guard of its own.█// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {█// Persist this brain generation's stderr to the rotating SPT_HOME/logs sink█// (REQ-DAEMON-STDERR-PERSIST) - a detached brain's stdio is /dev/null, so every█// diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs█// FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.█// [impl->REQ-DAEMON-STDERR-PERSIST]█let _ = crate::stderrlog::install(\"brain\", generation);█let name = broker_socket_name();█let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;█// The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the█// 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█// con [...11789ch elided...] let child =█spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;█// Enroll the (re)spawned brain into the reap container so a█// `daemon stop` tears down the brain + its Psyches as one unit█// (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]█reaper.enroll(&child);█Ok(child)█},█);█});█(handle, restart)█}█#[cfg(test)]█mod tests {█use super::*;█use std::sync::atomic::AtomicU32;█/// A trivial, fast-exiting real child - the cross-OS no-op the supervisor█/// test respawns (stands in for the brain process).█fn fast_child() -> io::Result<Child> {█#[cfg(windows)]█{█...█}█...█}█...█}\u000f█</out>█# Tool call ¶█//Reading current resume methods█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"430-585\")█<out>█\u000e pub fn session_id(&self) -> Option<u64> {█...█}█/// Snapshot durable state for a future handoff. **Test-only (restoration█/// D4-3)** - the production path reconstructs from the broker, never a█/// snapshot frame; see [`BrainState`].█pub fn snapshot(&self) -> BrainState {█BrainState {█session_id: self.session_id.expect(\"snapshot before a session exists\"),█generation: self.generation,█next_seq: self.next_seq,█gen_start_ms: self.gen_start_ms,█}█}█/// Spawn a session under a broker PTY; the broker auto-subscribes this█/// connection. Reads until the `spawned` reply (consuming any output that█/// races ahead).█pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {█self.spawn_session_pid(req)█.map(|(session_id, _)| session_id)█}█/// [`spawn_session`](Self::spawn_session) that also surfaces the child's█/// OS pid (when the backend exposes one) - measurement/observability█/// callers (the D9-3 dormancy-budget harness) sample the seat by pid.█///█/// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below█[...5680ch elided...] d(█KIND_KILL,█serde_json::to_value(KillReq { session_id: sid }).expect(\"KillReq serializes\"),█)█}█/// Read output events until the accumulated bytes contain `needle`,█/// appending all received output to `buf`. Returns the highest seq seen.█/// A session `Exit` before the needle is found is an error (the child died).█pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {█loop {█if contains(buf, needle) {█return Ok(self.next_seq.saturating_sub(1));█}█match self.read_event()? {█BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),█...█}█}█}█[Showing lines 429-588 of 2178. Use :589 to continue]\u000f█</out>█# Tool call ¶█//Reading bounded connection writes█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"1-330\")█<out>█\u000e//! Bounded, cancelable, poison-on-failure framed writes for one physical█//! broker connection (UPDATE-WEDGE round-4).█//!█//! ## The failure class this kills█//!█//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard█//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes█//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` - a local-socket█//! path with **no supported write timeout** (`set_timeout` -> `Unsupported`).█//! A connected consumer that stops reading (an `rc --take` whose process█//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the█//! logical stall-evict (`stall_evict_controller`) releases only the controller█//! ROLE - it never cancels the in-flight pipe write or closes the physical█//! connection, so the writer (and every write queued behind the same gate)█//! stays wedged until the peer process dies (~128 s in the field capture, on a█//! brain restart).█//!█//! ## The primitive█//!█//! [`BrokerConn`] owns the send half outright - it never leaves this module,█//! so an unbounded bypass write cannot compile. Every write:█//!█//! 1. acquires the serialized write GATE with a bounded wait (an [...13848ch elided...] t slipped-in read. On Unix the█/// `shutdown` in [`poison_and_cancel`] already did all of this.█// On Unix the cfg(windows) teardown strips out and the idempotence█// guard's early `return` becomes the function tail - needless_return█// there, load-bearing here. Restructuring (e.g. `if !swapped { ... }`)█// would leave Unix an empty if-body (`clippy::needless_if`) instead.█#[cfg_attr(unix, allow(clippy::needless_return))]█fn sever(&self) {█if self.severed.swap(true, Ordering::AcqRel) {█return;█}█#[cfg(windows)]█// SAFETY: as in poison_and_cancel - handle outlives the watchdog.█unsafe {█let _ = win::DisconnectNamedPipe(self.raw);█...█}█}█...█}█[Showing lines 1-333 of 815. Use :334 to continue]\u000f█</out>█# Tool call ¶█//Reading resume deadlock test█read(path=\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\", selector=\"1-360\")█<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 ¶█//Reading false promotion test█read(path=\"crates/spt-daemon/tests/false_promote.rs\", selector=\"1-340\")█<out>█\u000e//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) - the FALSE-PROMOTE int rig, on a█//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the█//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` -> `run_trial`), the gate█//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.█//!█//! ## The bug this gate locks down (the 22:47 false-promote)█//!█//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-█//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) - it does NOT close it. A new brain█//! can signal `brain.ready` INSIDE that window while the OLD generation's local█//! controller conn is still wedged (blocked writes pending), so `update apply`█//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The█//! fix: the promotion gate adds a DRAINED precondition - promote only on ready AND█//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),█//! read from BROKER TRUTH in-process (no brain round-trip).█//!█//! ## The wedge analog (round-4 revision - statically held, doyle Option C)█//!█//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside█[...12215ch elided...] e OLD generation's conn is still wedged\"█);█assert!(█env.rollbacks.lock().unwrap().is_empty(),█\"and it must not have rolled back yet - the window is far from elapsed\"█);█// -- Drain the old-gen conn: a second operator's take stall-evicts the wedged local█// incumbent broker-side (the W2 self-heal). any_local_controller_wedged now false. --█remote_take(&name, sid);█// Poll for the promotion the drain unblocks (bounded - the gate flips within a tick█// of the evict; a stall-evict is prompt, not a hang).█let deadline = Instant::now() + Duration::from_secs(8);█let promoted = loop {█if !env.promotions.lock().unwrap().is_empty() {█break true;█}█...█};█...█}█[Showing lines 1-343 of 376. Use :344 to continue]\u000f█</out>"}}}
{"type":"message","id":"cffe937d","parentId":"b66f1235","timestamp":"2026-07-16T09:57:09.642Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reading relevant lines for transitions**"},{"type":"toolCall","id":"call_PinQNhsEnr0sa8AuC6xQ3YA1|fc_0698befac979932d016a58aaf53144819aa1079beecc6a1b8c","name":"read","arguments":{"i":"Reading refresh CLI exact flow","path":"crates/spt/src/cli.rs","selector":"545-600,1435-1490,3940-4095"}},{"type":"toolCall","id":"call_RjjmpclhZJYCH0B6a24EXmT7|fc_0698befac979932d016a58aaf53154819a84ce6ed41661849a","name":"read","arguments":{"i":"Reading update apply dispatch","path":"crates/spt/src/cli.rs","selector":"4875-5235"}},{"type":"toolCall","id":"call_X3RhsLUJsd016ZRcvEqkDZZQ|fc_0698befac979932d016a58aaf53160819a931a2983ff7aaeb1","name":"read","arguments":{"i":"Reading update apply orchestrator","path":"crates/spt-daemon/src/applyhost.rs","selector":"1-125,248-385"}},{"type":"toolCall","id":"call_eu665FXO6rUEyecAkJyvl4vn|fc_0698befac979932d016a58aaf53168819ab5ec079740e4fb7f","name":"read","arguments":{"i":"Reading broker restart dispatch","path":"crates/spt-daemon/src/broker.rs","selector":"820-890,2895-3025,3760-3915"}},{"type":"toolCall","id":"call_v2ZuKFTNvhGoOPQxjXKlt9np|fc_0698befac979932d016a58aaf5316c819a9b67e157ccfe414c","name":"read","arguments":{"i":"Reading broker daemon supervisor boot","path":"crates/spt-daemon/src/daemon.rs","selector":"155-255,450-610"}},{"type":"toolCall","id":"call_P1VYsQSjta6FcSZwrY1nfq15|fc_0698befac979932d016a58aaf53188819a967c3526669dfe4c","name":"read","arguments":{"i":"Reading supervisor state machine","path":"crates/spt-daemon/src/brainproc.rs","selector":"1-185,175-315,650-865,920-1075"}},{"type":"toolCall","id":"call_rMOR3nbMe5h5hZNYnqUIxu3x|fc_0698befac979932d016a58aaf53198819aa7a34d6e064b1ad7","name":"read","arguments":{"i":"Reading daemon refresh e2e","path":"crates/spt/tests/daemon_refresh_e2e.rs","selector":"1-285"}},{"type":"toolCall","id":"call_bB22GSLbuCScc8bZdv3E2ej6|fc_0698befac979932d016a58aaf531a4819a9f8231624d5f7210","name":"grep","arguments":{"i":"Finding inherited handle posture","pattern":"Stdio::null|Stdio::inherit|DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW|close_fds|pre_exec|JobObject|reaper|spawn\\(\\)","path":"crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/src/daemon.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/applyhost.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":46970,"output":463,"cacheRead":0,"cacheWrite":0,"totalTokens":47433,"reasoningTokens":18,"cost":{"input":0.23485000000000003,"output":0.01389,"cacheRead":0,"cacheWrite":0,"total":0.24874000000000002}},"stopReason":"toolUse","timestamp":1784195809052,"responseId":"resp_0698befac979932d016a58aae1c8c8819a85230c21eccbccfa","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58aaec743c819aa9fa5efa69fe3826","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKr1jvh0LCa9sSJ73mHqvsiQIgu5Ydours92JlTAY7PvD2PD0LfyZxP5uTfnM6zjMCqnHd9Uim_mhcZnroLsqxPT-vWUNQDFppN1eAVmuyAK2QXLL1l8lGlNyCGc_v43Su16DHS-v3LNb3DjcH9j29hPGH1Z72qsPjRI1QCtIiPodwHpSUx-A4USQy5bXX6cjDw5W7KO1EpexsG-YF5_1R7NHhLB7241hNgyqguYIukIBgUI5stmc-24TwmNWi4AuBllViMNE-fsCj4W3oqAwUpUF8CWgPk5pMabDvo8p6tFjlw1-p0xmTTrK1bsKRXRay8yWZR3PE0VGWL-wvnC6_Q2DFXGttJ4g1cGjwMte4Nh01V2PwobToZ_xj9wrcnGPH_oauRvrqLNrFoXnrqeEcgJkNMsaAd-lJmFbanSP7D7boF40cCJzgXiVzLr1WtdNlycThteOTkfimumBUmfC8G_-uqvyxSqArQ1dYitKWcH06KcU0HT9dqZbZLNeByyFYIR-xBLwlipD3Lq_CEikOmfSOGy8ZMNLCgRakXQm5qcqhicz_2RW6_-b5JKnx9oTF16rC3tAwTRQTjYC66oYmpdLMtIz9Qd8bSYLxaP2Bowb0EB5i4CSveCXqpAvWxwm-bT-b41uHRbAWQ_RVF5sWvjkJNT84PA0XppTLLB-ReJAwxgkELgzU-0SYcE5eu6phkIkmWL7E2sh5aKs4hpiv88FRg87mVSsVFkH5Ax7oslX_rsAuTncta7ngv2T4CJZ8e20oGZfcsxozsjlZH86EJwrRcmr_u9rl9e4Qh1bS6RaYOcpceA7IpYzNo5QqY39YJX0H8y3KSQWIQo07lv_cNOcXfL1OdKQYE90yWPMWoLKPDxV4yXKuMOyqVPsFELu4nyD405bSOu7RzdEkMfRHrDNmHcF-imM6h-zLTY2jJZKrsOJZ93lK-PfhIqb3s4xadBpBUARxkUi2mST-UGGxmYqtshs4Bd5eEcS0L-IAeyslW7PPboWxl816cAXyO_gQzjJQxY2PSLlMBrPv1b5jVeuQ==","summary":[{"type":"summary_text","text":"**Reading relevant lines for transitions**"}]},{"id":"fc_0698befac979932d016a58aaf53144819aa1079beecc6a1b8c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh CLI exact flow\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"545-600,1435-1490,3940-4095\"}","call_id":"call_PinQNhsEnr0sa8AuC6xQ3YA1","name":"read"},{"id":"fc_0698befac979932d016a58aaf53154819a84ce6ed41661849a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update apply dispatch\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"4875-5235\"}","call_id":"call_RjjmpclhZJYCH0B6a24EXmT7","name":"read"},{"id":"fc_0698befac979932d016a58aaf53160819a931a2983ff7aaeb1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update apply orchestrator\",\"path\":\"crates/spt-daemon/src/applyhost.rs\",\"selector\":\"1-125,248-385\"}","call_id":"call_X3RhsLUJsd016ZRcvEqkDZZQ","name":"read"},{"id":"fc_0698befac979932d016a58aaf53168819ab5ec079740e4fb7f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker restart dispatch\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"820-890,2895-3025,3760-3915\"}","call_id":"call_eu665FXO6rUEyecAkJyvl4vn","name":"read"},{"id":"fc_0698befac979932d016a58aaf5316c819a9b67e157ccfe414c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker daemon supervisor boot\",\"path\":\"crates/spt-daemon/src/daemon.rs\",\"selector\":\"155-255,450-610\"}","call_id":"call_v2ZuKFTNvhGoOPQxjXKlt9np","name":"read"},{"id":"fc_0698befac979932d016a58aaf53188819a967c3526669dfe4c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading supervisor state machine\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"1-185,175-315,650-865,920-1075\"}","call_id":"call_P1VYsQSjta6FcSZwrY1nfq15","name":"read"},{"id":"fc_0698befac979932d016a58aaf53198819aa7a34d6e064b1ad7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon refresh e2e\",\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"selector\":\"1-285\"}","call_id":"call_rMOR3nbMe5h5hZNYnqUIxu3x","name":"read"},{"id":"fc_0698befac979932d016a58aaf531a4819a9f8231624d5f7210","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding inherited handle posture\",\"pattern\":\"Stdio::null|Stdio::inherit|DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW|close_fds|pre_exec|JobObject|reaper|spawn\\\\(\\\\)\",\"path\":\"crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/src/daemon.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/applyhost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_bB22GSLbuCScc8bZdv3E2ej6","name":"grep"}]},"duration":20589.335599999875,"ttft":11381.41019999981,"contextSnapshot":{"promptTokens":46970,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PinQNhsEnr0sa8AuC6xQ3YA1|fc_0698befac979932d016a58aaf53144819aa1079beecc6a1b8c","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading refresh CLI exact flow"},"id":"5f6d35db","parentId":"cffe937d","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RjjmpclhZJYCH0B6a24EXmT7|fc_0698befac979932d016a58aaf53154819a84ce6ed41661849a","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading update apply dispatch"},"id":"903bab95","parentId":"5f6d35db","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_X3RhsLUJsd016ZRcvEqkDZZQ|fc_0698befac979932d016a58aaf53160819a931a2983ff7aaeb1","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt-daemon/src/applyhost.rs"},"intent":"Reading update apply orchestrator"},"id":"563859e5","parentId":"903bab95","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eu665FXO6rUEyecAkJyvl4vn|fc_0698befac979932d016a58aaf53168819ab5ec079740e4fb7f","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker restart dispatch"},"id":"9e38c8bd","parentId":"563859e5","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_v2ZuKFTNvhGoOPQxjXKlt9np|fc_0698befac979932d016a58aaf5316c819a9b67e157ccfe414c","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt-daemon/src/daemon.rs"},"intent":"Reading broker daemon supervisor boot"},"id":"369c1082","parentId":"9e38c8bd","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_P1VYsQSjta6FcSZwrY1nfq15|fc_0698befac979932d016a58aaf53188819a967c3526669dfe4c","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading supervisor state machine"},"id":"36c69e0a","parentId":"369c1082","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rMOR3nbMe5h5hZNYnqUIxu3x|fc_0698befac979932d016a58aaf53198819aa7a34d6e064b1ad7","toolName":"read","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt/tests/daemon_refresh_e2e.rs"},"intent":"Reading daemon refresh e2e"},"id":"3921caee","parentId":"36c69e0a","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bB22GSLbuCScc8bZdv3E2ej6|fc_0698befac979932d016a58aaf531a4819a9f8231624d5f7210","toolName":"grep","startedAt":"2026-07-16T09:57:09.643Z","args":{"path":"crates/spt-daemon/src/brainproc.rs;crates/spt-daemon/src/daemon.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/applyhost.rs"},"intent":"Finding inherited handle posture"},"id":"e9eb7766","parentId":"3921caee","timestamp":"2026-07-16T09:57:09.643Z"}
{"type":"message","id":"ef6deebf","parentId":"e9eb7766","timestamp":"2026-07-16T09:57:09.654Z","message":{"role":"toolResult","toolCallId":"call_X3RhsLUJsd016ZRcvEqkDZZQ|fc_0698befac979932d016a58aaf53160819a931a2983ff7aaeb1","toolName":"read","content":[{"type":"text","text":"//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n//! (\"nothing in the daemon invokes this apply after the user acks the consent\n//! notif\").\n//!\n//! The ack IS the verb the consent notif names: its body says \"confirm to\n//! apply (`spt update apply`) or dismiss\" (ADR-0007 — the harness's native\n//! confirm affordance answers by running the command; dismiss latches the\n//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n//! still works, the user changed their mind).\n//!\n//! [`apply_staged`] drives the whole chain against what the peer pump left\n//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):\n//!\n//! 1. **Re-verify at rest** (`plan_verified` under the production\n//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n//!    or expired artifact refuses here. The monotonic-version floor was\n//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n//! 2. **Classify** against the running broker's ABI. Only\n//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n//!    orchestration is post-M5; ADR-0004 §A).\n//! 3. **Swap the binary**: the current executable steps aside\n//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n//!    OSes) and the verified artifact lands at the executable path\n//!    atomically. A failed landing rolls the original back — never a\n//!    half-swapped seat.\n//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],\n//!    ADR-0018 D3-3): the broker signals its supervisor to cycle the brain\n//!    child onto the just-swapped binary with `start-reason=update`. No hosted\n//!    endpoint terminates or suspends (REQ-UPD-3, absolute for this class — the\n//!    broker holds them all across the cycle). This replaces the old in-process\n//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n//!    subscriber while the live brain kept running the OLD code.\n//!\n//! New code runs **immediately** — the supervised respawn lands on the new\n//! executable, no manual bounce, no logon. Readiness-gated promotion +\n//! auto-rollback on a brain that fails to come up is D6.\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\n\nuse std::io;\nuse std::path::Path;\nuse std::time::Duration;\n\nuse crate::brain::{now_ms, Brain};\nuse crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\nuse crate::release::{current_platform, VerifyPolicy};\nuse crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n\n/// What [`apply_staged`] did — loud + typed, never silent.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum ApplyStagedOutcome {\n    /// Nothing staged (or a torn stage that never offers) — idempotent no-op.\n    NoUpdate,\n    /// The staged version is already the running/applied image — an idempotent\n    /// exit-0 no-op (F-025). Short-circuits BEFORE the binary-aside rename, whose\n    /// re-run on an already-swapped exe is the 'Access is denied (os error 5)'\n    /// this fixes. `version`/`product_version` name what is already installed.\n    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n    AlreadyApplied {\n        version: u64,\n        product_version: String,\n    },\n    /// The at-rest re-verification refused (tamper / expiry / key trust) —\n    /// nothing swapped, the reason says why.\n    Rejected(String),\n    /// Verified but not brain-only — the M5 apply surface refuses the\n    /// broker-touching classes typed; the artifact stays staged.\n    RefusedClass(UpdateClass),\n    /// Refused: this version was auto-rolled-back on this node (a prior trial\n    /// failed readiness) and is **quarantined** — never re-applied until a newer\n    /// version stages or an operator forces it (the deferred escape hatch). The\n    /// daemon stays healthy on the rolled-back binary (ADR-0018 Q7, D6-2).\n    Quarantined { version: u64 },\n    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n    /// provisional immediately (it cannot observe the asynchronous verdict — the\n    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n    /// be the optimistic enlyzeam write D6 exists to kill. `version` is the\n    /// monotonic counter; `product_version` the human-facing semver (empty for\n    /// pre-v0.3.2 metadata, whence the counter-based fallback message).\n    // [impl->REQ-UPD-6]\n    // [impl->REQ-UPD-8]\n    AppliedPending {\n        version: u64,\n        product_version: String,\n    },\n    /// Swapped the binary on disk with NO running daemon touched — a\n    /// **daemonless** apply (REQ-UPDATE-ONE-SHOT-FINISH). No broker was booted to\n    /// hand off to (the old-broker-boot-pre-swap wart) and no brain was cycled;\n    /// the `AppliedPending` trial is recorded so the NEXT daemon start boots the\n    /// new bytes and promotes it on the fresh brain's `ready`. `version` is the\n    /// monotonic counter; `product_version` the human semver (empty pre-v0.3.2).\n    // [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n    AppliedDaemonless {\n        version: u64,\n        product_version: String,\n    },\n}\n\n/// Either an early terminal apply outcome (nothing swaps) or the go-ahead to\n/// swap, carrying the verified bytes. The shared verify/classify preamble both\n/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on\n/// whether a live broker is handed off to.\nenum Prepared {\n    /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /\n    /// Quarantined / Rejected / RefusedClass) — return it verbatim.\n    Done(ApplyStagedOutcome),\n    /// Verified brain-only bytes ready to land at the exe path.\n    Proceed {\n        version: u64,\n        product_version: String,\n        artifact: Vec<u8>,\n    },\n}\n\n/// The verify + classify preamble (module docs steps 1-2): quarantine guard,\n/// already-applied short-circuit, at-rest re-verification, platform guard, and\n/// the brain-only class gate. Pure w.r.t. the daemon — reads only the cache +\n/// keys, swaps nothing. Both the broker-handoff apply and the daemonless apply\n/// run this identical gate so their fail-closed posture never diverges.\n// [impl->REQ-UPD-4]\n…\n/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n/// whether a running brain is cycled afterwards.\n// [impl->REQ-UPD-6]\nfn swap_and_record(\n    cache: &ReleaseCache,\n    exe_path: &Path,\n    version: u64,\n    artifact: &[u8],\n) -> Result<(), String> {\n    // Swap the binary: step aside, land the artifact, roll back on failure.\n    let aside = swap_aside_path(exe_path, version);\n    let _ = std::fs::remove_file(&aside); // stale prior apply of this version\n    std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;\n    if let Err(e) = land_artifact(exe_path, artifact) {\n        let _ = std::fs::rename(&aside, exe_path); // roll back — never half-swapped\n        return Err(format!(\"land new binary (rolled back): {e}\"));\n    }\n    // The binary IS swapped from here on — but DO NOT claim `applied` yet\n    // (ADR-0018 Q7, restoration D6-1): the new brain has not booted, let alone\n    // reached `ready`. Writing `applied` here is the optimism the design indicts\n    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n    // carrying the last-known-good `rollback_binary` (the aside we just created)\n    // so the broker's supervisor, a different process from this CLI, can promote\n    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n    // [impl->REQ-UPD-6]\n    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n        version,\n        rollback_binary: aside.to_string_lossy().into_owned(),\n        candidate_started_ms: now_ms(),\n        // The version actually running before this swap — what a rollback returns\n        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n        // the quarantined version, not the running one. `None` when nothing was\n        // ever recorded applied (a first-ever apply) → the supervisor falls back\n        // to N-1, the only honest guess then.\n        prior_version: cache.applied_version(),\n    });\n    let _ = cache.record_last_outcome(\"apply-pending\");\n    Ok(())\n}\n\n/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n/// the canonical broker socket the brain handoff runs against. For a stopped box,\n/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\npub fn apply_staged(\n    cache: &ReleaseCache,\n    release_keys: &Path,\n    exe_path: &Path,\n    broker_name: &str,\n) -> Result<ApplyStagedOutcome, String> {\n    let (version, product_version, artifact) = match prepare_apply(cache, release_keys)? {\n        Prepared::Done(outcome) => return Ok(outcome),\n        Prepared::Proceed {\n            version,\n            product_version,\n            artifact,\n        } => (version, product_version, artifact),\n    };\n\n    // Connect to the broker BEFORE the binary moves — a dead broker fails the\n    // apply with the seat untouched. The connection is the channel the planned\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/// the swapped bytes run when the daemon is NEXT started (either by the operator,\n/// or by `update apply --finish` which restarts it in the same command, riding\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so hosted endpoints come back). The\n/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n/// the handoff path's does. Runs the identical verify/classify gate as\n/// [`apply_staged`], so every fail-closed refusal is preserved.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n// [impl->REQ-UPD-4]\npub fn apply_staged_daemonless(\n    cache: &ReleaseCache,\n…\n) -> Result<ApplyStagedOutcome, String> {\n…\n}"}],"details":{"displayContent":{"text":"//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n//! (\"nothing in the daemon invokes this apply after the user acks the consent\n//! notif\").\n//!\n//! The ack IS the verb the consent notif names: its body says \"confirm to\n//! apply (`spt update apply`) or dismiss\" (ADR-0007 — the harness's native\n//! confirm affordance answers by running the command; dismiss latches the\n//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n//! still works, the user changed their mind).\n//!\n//! [`apply_staged`] drives the whole chain against what the peer pump left\n//! behind (REQ-UPD-4: staged + consent-notified, never auto-applied):\n//!\n//! 1. **Re-verify at rest** (`plan_verified` under the production\n//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n//!    or expired artifact refuses here. The monotonic-version floor was\n//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n//! 2. **Classify** against the running broker's ABI. Only\n//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n//!    orchestration is post-M5; ADR-0004 §A).\n//! 3. **Swap the binary**: the current executable steps aside\n//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n//!    OSes) and the verified artifact lands at the executable path\n//!    atomically. A failed landing rolls the original back — never a\n//!    half-swapped seat.\n//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],\n//!    ADR-0018 D3-3): the broker signals its supervisor to cycle the brain\n//!    child onto the just-swapped binary with `start-reason=update`. No hosted\n//!    endpoint terminates or suspends (REQ-UPD-3, absolute for this class — the\n//!    broker holds them all across the cycle). This replaces the old in-process\n//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n//!    subscriber while the live brain kept running the OLD code.\n//!\n//! New code runs **immediately** — the supervised respawn lands on the new\n//! executable, no manual bounce, no logon. Readiness-gated promotion +\n//! auto-rollback on a brain that fails to come up is D6.\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\n\nuse std::io;\nuse std::path::Path;\nuse std::time::Duration;\n\nuse crate::brain::{now_ms, Brain};\nuse crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\nuse crate::release::{current_platform, VerifyPolicy};\nuse crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n\n/// What [`apply_staged`] did — loud + typed, never silent.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum ApplyStagedOutcome {\n    /// Nothing staged (or a torn stage that never offers) — idempotent no-op.\n    NoUpdate,\n    /// The staged version is already the running/applied image — an idempotent\n    /// exit-0 no-op (F-025). Short-circuits BEFORE the binary-aside rename, whose\n    /// re-run on an already-swapped exe is the 'Access is denied (os error 5)'\n    /// this fixes. `version`/`product_version` name what is already installed.\n    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n    AlreadyApplied {\n        version: u64,\n        product_version: String,\n    },\n    /// The at-rest re-verification refused (tamper / expiry / key trust) —\n    /// nothing swapped, the reason says why.\n    Rejected(String),\n    /// Verified but not brain-only — the M5 apply surface refuses the\n    /// broker-touching classes typed; the artifact stays staged.\n    RefusedClass(UpdateClass),\n    /// Refused: this version was auto-rolled-back on this node (a prior trial\n    /// failed readiness) and is **quarantined** — never re-applied until a newer\n    /// version stages or an operator forces it (the deferred escape hatch). The\n    /// daemon stays healthy on the rolled-back binary (ADR-0018 Q7, D6-2).\n    Quarantined { version: u64 },\n    /// Swapped + brain-restart **triggered** — the new code is now **on trial**,\n    /// not yet promoted (ADR-0018 Q7, D6-2): the supervisor promotes it on the\n    /// brain's `ready` signal or auto-rolls-back on failure. `apply` returns\n    /// provisional immediately (it cannot observe the asynchronous verdict — the\n    /// rollback notif / `debug-converge` carry it); claiming `applied` here would\n    /// be the optimistic enlyzeam write D6 exists to kill. `version` is the\n    /// monotonic counter; `product_version` the human-facing semver (empty for\n    /// pre-v0.3.2 metadata, whence the counter-based fallback message).\n    // [impl->REQ-UPD-6]\n    // [impl->REQ-UPD-8]\n    AppliedPending {\n        version: u64,\n        product_version: String,\n    },\n    /// Swapped the binary on disk with NO running daemon touched — a\n    /// **daemonless** apply (REQ-UPDATE-ONE-SHOT-FINISH). No broker was booted to\n    /// hand off to (the old-broker-boot-pre-swap wart) and no brain was cycled;\n    /// the `AppliedPending` trial is recorded so the NEXT daemon start boots the\n    /// new bytes and promotes it on the fresh brain's `ready`. `version` is the\n    /// monotonic counter; `product_version` the human semver (empty pre-v0.3.2).\n    // [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n    AppliedDaemonless {\n        version: u64,\n        product_version: String,\n    },\n}\n\n/// Either an early terminal apply outcome (nothing swaps) or the go-ahead to\n/// swap, carrying the verified bytes. The shared verify/classify preamble both\n/// [`apply_staged`] and [`apply_staged_daemonless`] run before they diverge on\n/// whether a live broker is handed off to.\nenum Prepared {\n    /// A terminal outcome reached before any swap (NoUpdate / AlreadyApplied /\n    /// Quarantined / Rejected / RefusedClass) — return it verbatim.\n    Done(ApplyStagedOutcome),\n    /// Verified brain-only bytes ready to land at the exe path.\n    Proceed {\n        version: u64,\n        product_version: String,\n        artifact: Vec<u8>,\n    },\n}\n\n/// The verify + classify preamble (module docs steps 1-2): quarantine guard,\n/// already-applied short-circuit, at-rest re-verification, platform guard, and\n/// the brain-only class gate. Pure w.r.t. the daemon — reads only the cache +\n/// keys, swaps nothing. Both the broker-handoff apply and the daemonless apply\n/// run this identical gate so their fail-closed posture never diverges.\n// [impl->REQ-UPD-4]\n…\n/// Swap the binary at `exe_path` (step aside → land → roll back on failure) and\n/// record the open `AppliedPending` trial. Shared by the broker-handoff apply\n/// and the daemonless apply — both land the SAME bytes and record the SAME trial\n/// (the supervisor promotes it on the next brain's `ready`); they differ only in\n/// whether a running brain is cycled afterwards.\n// [impl->REQ-UPD-6]\nfn swap_and_record(\n    cache: &ReleaseCache,\n    exe_path: &Path,\n    version: u64,\n    artifact: &[u8],\n) -> Result<(), String> {\n    // Swap the binary: step aside, land the artifact, roll back on failure.\n    let aside = swap_aside_path(exe_path, version);\n    let _ = std::fs::remove_file(&aside); // stale prior apply of this version\n    std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;\n    if let Err(e) = land_artifact(exe_path, artifact) {\n        let _ = std::fs::rename(&aside, exe_path); // roll back — never half-swapped\n        return Err(format!(\"land new binary (rolled back): {e}\"));\n    }\n    // The binary IS swapped from here on — but DO NOT claim `applied` yet\n    // (ADR-0018 Q7, restoration D6-1): the new brain has not booted, let alone\n    // reached `ready`. Writing `applied` here is the optimism the design indicts\n    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n    // carrying the last-known-good `rollback_binary` (the aside we just created)\n    // so the broker's supervisor, a different process from this CLI, can promote\n    // to `applied` on the brain's `ready` signal or roll back on failure (D6-2).\n    // Best-effort: an unrecorded phase degrades the readiness gate, never the\n    // swap. The legacy `applied.json`/`last-outcome=applied` writes move to the\n    // supervisor's ready-promotion (D6-2) — here we only mark the trial open.\n    // [impl->REQ-UPD-6]\n    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n        version,\n        rollback_binary: aside.to_string_lossy().into_owned(),\n        candidate_started_ms: now_ms(),\n        // The version actually running before this swap — what a rollback returns\n        // to. The supervisor must NOT derive it as N-1 (a skip-version update,\n        // applied v5 → candidate v8, would mis-claim v7); the aside name encodes\n        // the quarantined version, not the running one. `None` when nothing was\n        // ever recorded applied (a first-ever apply) → the supervisor falls back\n        // to N-1, the only honest guess then.\n        prior_version: cache.applied_version(),\n    });\n    let _ = cache.record_last_outcome(\"apply-pending\");\n    Ok(())\n}\n\n/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n/// the canonical broker socket the brain handoff runs against. For a stopped box,\n/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPD-4]\npub fn apply_staged(\n    cache: &ReleaseCache,\n    release_keys: &Path,\n    exe_path: &Path,\n    broker_name: &str,\n) -> Result<ApplyStagedOutcome, String> {\n    let (version, product_version, artifact) = match prepare_apply(cache, release_keys)? {\n        Prepared::Done(outcome) => return Ok(outcome),\n        Prepared::Proceed {\n            version,\n            product_version,\n            artifact,\n        } => (version, product_version, artifact),\n    };\n\n    // Connect to the broker BEFORE the binary moves — a dead broker fails the\n    // apply with the seat untouched. The connection is the channel the planned\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/// the swapped bytes run when the daemon is NEXT started (either by the operator,\n/// or by `update apply --finish` which restarts it in the same command, riding\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so hosted endpoints come back). The\n/// recorded `AppliedPending` promotes on that fresh brain's `ready`, exactly as\n/// the handoff path's does. Runs the identical verify/classify gate as\n/// [`apply_staged`], so every fail-closed refusal is preserved.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n// [impl->REQ-UPD-4]\npub fn apply_staged_daemonless(\n    cache: &ReleaseCache,\n…\n) -> Result<ApplyStagedOutcome, String> {\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,null,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,null,388,null,402]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\applyhost.rs"}}},"isError":false,"timestamp":1784195829654}}
{"type":"message","id":"c94329bd","parentId":"ef6deebf","timestamp":"2026-07-16T09:57:09.777Z","message":{"role":"toolResult","toolCallId":"call_P1VYsQSjta6FcSZwrY1nfq15|fc_0698befac979932d016a58aaf53188819a967c3526669dfe4c","toolName":"read","content":[{"type":"text","text":"//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n//! broker/brain process-isolation restoration, D1 skeleton).\n//!\n//! ADR-0004 specified the broker/brain split as two **processes**; production\n//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n//! the binary on disk but never restarts the running code (the regression\n//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n//! `spt daemon brain` child. The child connects over the existing broker socket\n//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n//! verb. The digest hub serve + seed-control stay broker-side continuity\n//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n//! loops.\n//!\n//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n//! broker spawns + supervises a brain child over the socket, and the broker\n//! (with its PTY children + seed channel) survives the brain dying — the\n//! supervisor respawns it from the executable path, which on a real update is\n//! the swapped binary.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\nuse std::io;\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::endpoint::{brain_ready_path, broker_socket_name};\nuse crate::relcache::{AppliedRecord, ReleaseCache};\n\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n/// never a hot respawn loop.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n/// A run at least this long counts as healthy and resets the backoff floor.\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n/// brain has this long to signal `ready` for its generation before the window\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n/// **exits before** signaling ready is respawned this many consecutive times\n/// (a transient first-boot hiccup should not abandon a good binary) before the\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n/// window-expiry triggers rollback.\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// How long the brain child retries connecting to the broker socket before\n/// giving up (the broker may still be binding — the child is freshly spawned).\nconst BRAIN_CONNECT_TRIES: u32 = 300;\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\n/// reliable than a brain→brain frame, which retires in D4).\n///\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\n/// never a parse failure that would brick the brain before it rehydrates.\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum StartReason {\n    /// The broker's first brain spawn this broker lifetime.\n    Cold,\n    /// A supervised respawn after an unexpected brain exit.\n    Crash,\n    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n    Update,\n}\n\nimpl StartReason {\n    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n    pub fn as_arg(self) -> &'static str {\n        match self {\n            StartReason::Cold => \"cold\",\n            StartReason::Crash => \"crash\",\n            StartReason::Update => \"update\",\n        }\n    }\n\n    /// Parse the `--start-reason` argv value, lenient: an unknown token (a\n    /// newer broker's reason an older brain predates) defaults to `Cold` rather\n    /// than failing the parse (KH-2.3 — defaults survive intermediate versions).\n    pub fn from_arg(s: &str) -> Self {\n        match s {\n            \"update\" => StartReason::Update,\n            \"crash\" => StartReason::Crash,\n            _ => StartReason::Cold,\n        }\n    }\n}\n\n/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n/// its next wait tick, kills the current brain child, and respawns it with\n/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n/// between the broker serve path and the supervisor — both run in the broker\n/// process. This is the seam that makes a routine `apply` actually run new code,\n/// the pillar the in-process-collapse regression silently unrealized.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n#[derive(Default)]\npub struct BrainRestart {\n    requested: AtomicBool,\n}\n\nimpl BrainRestart {\n    /// A fresh signal with no pending request.\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Request a planned restart (broker handler side) — idempotent: coalesces\n    /// with any already-pending request into one cycle.\n    pub fn request(&self) {\n        self.requested.store(true, Ordering::SeqCst);\n    }\n\n    /// Consume a pending request (supervisor side): true at most once per\n    /// `request`, clearing it so the next cycle starts fresh.\n    pub fn take(&self) -> bool {\n        self.requested.swap(false, Ordering::SeqCst)\n    }\n}\n\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n/// holding the connection and probing broker liveness each heartbeat. A dropped\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n/// from the (possibly swapped) binary.\n///\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n…\nenum TrialStep {\n…\n    /// The caller MUST kill it before spawning the rollback binary (**A11** — never\n    /// two live brains).\n    WindowElapsedAlive,\n    /// `stop` was raised during the trial — the caller kills + returns.\n    Stopped,\n}\n\n/// Watch a freshly-spawned trial candidate for readiness within `window`\n/// (ADR-0018 Q7, D6-2). Polls in `TICK` slices (so a daemon stop stays prompt,\n/// A11/note-ii) for a `brain.ready` stamp **== this spawn's `generation`** — a\n/// stale stamp from another generation never satisfies the gate (the single\n/// load-bearing correctness invariant; the A9 clear-before-spawn closes the\n/// cross-lifetime exact-generation hole). Returns the moment readiness is seen\n/// **AND the old generation has drained**, the child exits pre-ready, the window\n/// elapses, or `stop` is raised.\n///\n/// **The DRAINED precondition (REQ-UPDATE-PROMOTE-DRAINED).** `brain.ready` alone is\n/// NOT a promote: the W2 stall-evict only BOUNDS the false-promote window to\n/// `BRAIN_WRITE_DEADLINE`, so a new brain can signal ready while the OLD generation's\n/// local controller conn is still wedged (the 22:47 apply \"promoted\" onto a frozen\n/// control plane). So readiness is LATCHED (`ready_seen` — a candidate that stamped\n/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n/// the candidate and rolls back (conservative: never a false-promote onto a wedge).\n/// The normal case drains well inside the window — W2 bounds a real drain to\n/// `BRAIN_WRITE_DEADLINE` (15 s) < [`SUPERVISE_HEALTHY_RUN`] (30 s) — and a cleanly-\n/// closed old conn drains at once. `child.try_wait()` is consulted only while\n/// NOT-yet-ready, so a ready-then-exited candidate still promotes once drained.\nfn run_trial(\n    child: &mut Child,\n    generation: u64,\n    window: Duration,\n    env: &dyn TrialEnv,\n    stop: &AtomicBool,\n) -> TrialStep {\n    let deadline = Instant::now() + window;\n    let mut ready_seen = false;\n    loop {\n        if stop.load(Ordering::Relaxed) {\n            return TrialStep::Stopped;\n        }\n        // Latch readiness FIRST (don't demote a ready-then-exit child) — but do NOT\n        // promote on it alone: the old generation's control plane must have drained.\n        if !ready_seen && env.ready_generation() == Some(generation) {\n            ready_seen = true;\n        }\n        if ready_seen && env.old_gen_drained() {\n            return TrialStep::Promoted;\n        }\n        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n        // once ready-seen we are waiting on the OLD gen to drain, and a ready-then-\n        // exited child still promotes (the exit is a normal later respawn).\n        if !ready_seen {\n            match child.try_wait() {\n                Ok(Some(_)) => return TrialStep::PreReadyExit,\n                Ok(None) => {}\n                // A wait error: treat as a pre-ready exit (fail toward retry, never\n                // a panic in the supervisor thread).\n                Err(e) => {\n                    eprintln!(\"BRAIN_TRIAL_WAIT_FAIL: {e}\");\n                    return TrialStep::PreReadyExit;\n                }\n            }\n        }\n        if Instant::now() >= deadline {\n            return TrialStep::WindowElapsedAlive;\n        }\n        thread::sleep(TICK);\n    }\n}\n\n/// Supervise the brain child: spawn it, wait for it, and respawn on exit after a\n/// capped, doubling backoff — until `stop` is raised, which kills the live child\n/// and returns. The process-level sibling of [`crate::pump::supervise_pump`]\n/// (ADR-0018 Q2: the broker is the always-up anchor that respawns the brain;\n/// Q3: a planned update is just the brain self-exiting on a path the supervisor\n/// already recovers from).\n///\n/// **Readiness-gated promotion + auto-rollback (ADR-0018 Q7, D6-2).** When the\n/// injected `env` reports an unpromoted `AppliedPending` record, the spawn is a\n/// **trial** (the latch is the *record*, **A8** — regardless of start reason):\n/// the supervisor clears `brain.ready` (**A9**), spawns the candidate, and waits\n/// for it to stamp ready for ITS generation within `window`. It **promotes** on\n/// ready (writes `Applied`, the candidate is now the accepted binary and a later\n/// crash is a normal respawn); it **rolls back** to the last-known-good binary\n/// on budget exhaustion — [`TRIAL_PREREADY_BUDGET`] consecutive pre-ready exits\n/// **or** one alive-but-never-ready window-expiry (which **kills** the candidate\n/// first, **A11** — never two live brains). Binary selection is **record-driven**\n/// (a `RolledBack` record spawns the good `.old-N` binary, surviving a reboot for\n/// free), so no file is renamed at the failure instant. Every hosted endpoint is\n/// untouched throughout — the broker holds them across the whole cycle.\n///\n/// Generic over `spawn_child` (which receives the **selected binary** — `None` =\n/// the default `current_exe`, `Some(path)` = the rollback binary) and `env`, so\n/// the unit harness injects fakes; production binds [`spawn_brain_child`] +\n/// [`ProductionTrialEnv`].\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn supervise_brain(\n    stop: &AtomicBool,\n    restart: &BrainRestart,\n    base: Duration,\n    env: &dyn TrialEnv,\n    window: Duration,\n    mut spawn_child: impl FnMut(u64, StartReason, Option<&Path>) -> io::Result<Child>,\n) {\n    let mut backoff = base;\n    // Broker-owned generation custody (ADR-0018 V2, KH-2.4): the counter lives in\n    // the never-restarting supervisor and increments on EVERY spawn (cold or\n    // respawn), so the broker — which observes every respawn — is the\n    // authoritative source, not the outgoing brain. The first spawn is `Cold`;\n    // every supervised respawn here is a `Crash` (an unexpected exit the\n    // supervisor recovers from). A *planned* update respawn stamps `Update`\n    // instead — wired in D3-3, where the apply trigger drives this loop.\n    let mut generation: u64 = 0;\n    let mut reason = StartReason::Cold;\n    // Consecutive pre-ready exits of the CURRENT trial candidate (A8 budget K).\n    // In-memory: a broker restart resets it — acceptable, the trial restarts.\n    let mut pre_ready_exits: u32 = 0;\n    while !stop.load(Ordering::Relaxed) {\n        let started = Instant::now();\n        // Record-driven binary selection + trial latch (A8). The latch is the\n        // unpromoted `AppliedPending` record, NOT the start reason; a `RolledBack`\n        // record selects the last-known-good binary (and is never a trial — it is\n        // the recovery steady state). A missing/corrupt record degrades to \"no\n        // trial, default binary\" — the supervisor never panics on the record.\n        let record = env.applied_state();\n        let binary: Option<PathBuf> = match &record {\n            Some(AppliedRecord::RolledBack { rollback_binary, .. }) => {\n                Some(PathBuf::from(rollback_binary))\n            }\n            _ => None,\n        };\n        let is_trial = matches!(record, Some(AppliedRecord::AppliedPending { .. }));\n        // A9: clear `brain.ready` immediately before a trial spawn — the prior\n        // child is already waited-on dead (below), so the only stamp that can\n        // exist post-clear is THIS trial child's. Closes the cross-lifetime hole\n        // that same-generation equality alone cannot (a stale gen-N file from a\n        // pre-reboot lifetime matching a fresh gen-N trial).\n        if is_trial {\n            env.clear_ready();\n        }\n        // Did this cycle end because of a planned update restart (Q3) rather than\n        // an unexpected crash? The broker — observing the cycle it initiated — is\n        // the authoritative source of that distinction (V2).\n        let mut planned = false;\n        match spawn_child(generation, reason, binary.as_deref()) {\n            Ok(mut child) => {\n                // The trial gate runs first for a candidate on trial; on\n                // promotion it falls through to the normal wait loop for the SAME\n                // child. A non-trial spawn skips straight to normal supervision.\n                let mut supervise = !is_trial;\n                if is_trial {\n                    match run_trial(&mut child, generation, window, env, stop) {\n                        TrialStep::Stopped => {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        TrialStep::Promoted => {\n                            // Bytes-gate (KH 6.11): readiness != new-bytes. Promote\n                            // only if the candidate is running the staged bytes; a\n                            // ready-but-wrong-bytes candidate (e.g. a Linux respawn\n                            // onto the renamed old binary) fails the trial → kill +\n                            // rollback, never a falsely-`applied` record.\n                            let version = match &record {\n                                Some(AppliedRecord::AppliedPending { version, .. }) => Some(*version),\n                                _ => None,\n                            };\n                            if let Some(version) = version {\n                                if let BytesGate::Mismatch = bytes_gate(env, version) {\n                                    eprintln!(\"BRAIN_PROMOTE_REJECTED: candidate v{version} ready but running the WRONG bytes (generation {generation}) — rolling back (KH 6.11)\");\n                                    let _ = child.kill();\n                                    let _ = child.wait();\n                                    rollback(env, &record);\n                                    pre_ready_exits = 0;\n                                    generation += 1;\n                                    reason = StartReason::Crash;\n                                    backoff = base;\n                                    continue;\n                                }\n                                eprintln!(\"BRAIN_PROMOTED: candidate v{version} signalled ready (generation {generation}) — swap accepted\");\n                                env.record_promoted(version);\n                            }\n                            pre_ready_exits = 0;\n                            backoff = base;\n                            supervise = true; // keep supervising the now-accepted child\n                        }\n                        TrialStep::PreReadyExit => {\n                            pre_ready_exits += 1;\n                            eprintln!(\"BRAIN_TRIAL_EXIT: candidate exited pre-ready ({pre_ready_exits}/{TRIAL_PREREADY_BUDGET})\");\n                            if pre_ready_exits >= TRIAL_PREREADY_BUDGET {\n                                rollback(env, &record);\n                                pre_ready_exits = 0;\n                                // The rollback binary is known-good — respawn it\n                                // promptly, no crash-penalty backoff (mirror the\n                                // WindowElapsedAlive path); only the under-budget\n                                // candidate retries keep the doubling backoff.\n                                backoff = base;\n                            } else {\n                                backoff = next_backoff(backoff, started.elapsed(), base);\n                            }\n                            generation += 1;\n                            reason = StartReason::Crash;\n                            sleep_backoff(backoff, stop);\n                            continue;\n                        }\n                        TrialStep::WindowElapsedAlive => {\n                            // A11: KILL the alive-but-never-ready candidate BEFORE\n                            // spawning the rollback binary — two live brains would\n                            // break every single-brain seed/socket/IPC assumption.\n                            eprintln!(\"BRAIN_TRIAL_TIMEOUT: candidate alive but never ready — killing before rollback\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            rollback(env, &record);\n…\n                        }\n                    }\n                }\n…\n            }\n…\n        }\n…\n        if planned {\n…\n            backoff = base;\n            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n            continue;\n        }\n        reason = StartReason::Crash;\n        backoff = next_backoff(backoff, started.elapsed(), base);\n        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n        sleep_backoff(backoff, stop);\n    }\n}\n\n/// Effect the rollback for an exhausted trial (D6-2): from the candidate's\n/// `AppliedPending` record, write `RolledBack{quarantine=N, running, rollback_binary}`\n/// and fire the loud notif (via `env`). `running` is the candidate's\n/// `prior_version` (the version actually running before the swap, captured by\n/// `apply`) — NOT `N-1`, which is wrong on a skip-version update (applied v5 →\n/// candidate v8: the `.old-8` aside holds v5 bytes, so `running` is v5, not v7).\n/// It degrades to `N-1` only for a pre-D6-2b record that lacks `prior_version`.\n/// A record that is not `AppliedPending` (already rolled back, or raced away) is\n/// a no-op — never a double rollback.\n///\n/// **Rollback is SELECTION, not an update (open-call-4).** The recovery never\n/// routes through the update engine / `apply` / `verify_metadata`: it just makes\n/// the supervisor *spawn the `.old-N` binary*. So the anti-downgrade monotonic\n/// version floor (`REQ-HAZARD-UPDATE-ROLLBACK` — *attack* protection that refuses\n/// a version *downgrade*) **never sees** this recovery, and the two concerns stay\n/// cleanly separate. Do NOT \"fix\" `verify_metadata` to permit recovery\n/// downgrades — selection-not-apply is the mechanism, by design.\nfn rollback(env: &dyn TrialEnv, record: &Option<AppliedRecord>) {\n    if let Some(AppliedRecord::AppliedPending {\n        version,\n        rollback_binary,\n        prior_version,\n        ..\n    }) = record\n    {\n        let running = prior_version.unwrap_or(version.saturating_sub(1));\n        eprintln!(\"BRAIN_ROLLBACK: update v{version} failed readiness — rolling back to v{running} ({rollback_binary}), quarantining v{version}\");\n        env.record_rolled_back(*version, running, rollback_binary);\n    }\n}\n\n/// Promotion bytes-gate verdict (KH 6.11, `REQ-HAZARD-BRAIN-RESPAWN-PATH`).\nenum BytesGate {\n    /// Candidate is running the staged bytes — promote.\n    Match,\n    /// Candidate is ready but running the WRONG bytes — fail the trial, roll back.\n    Mismatch,\n    /// Either hash absent (pre-metadata release / missing breadcrumb) — degrade\n    /// to readiness-only promotion (N-1-safe).\n    Unverified,\n}\n\n/// Compare the just-ready candidate's stamped `exe_hash` against the staged\n/// artifact hash for `version` on this platform (KH 6.11). Readiness alone is\n/// NOT proof the new bytes run — on Linux a brain respawned onto the renamed\n/// old binary still signals ready (the v0.4.1 kitsubito false-success). On\n/// either hash absent the gate degrades to readiness-only but emits\n/// `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n        (Some(_), Some(_)) => BytesGate::Mismatch,\n        (expected, _) => {\n            let which = if expected.is_none() {\n                \"artifact-hash-absent\"\n            } else {\n                \"ready-hash-absent\"\n            };\n            eprintln!(\"PROMOTE_BYTES_UNVERIFIED:{version}:{which}\");\n            BytesGate::Unverified\n        }\n    }\n}\n\n/// Spawn the production brain child: re-exec `daemon brain` as a plain\n/// (non-detached) managed child, so it inherits the broker's environment —\n/// including `SPT_HOME` — and dies with no orphaning. NOT `spawn_detached`\n/// (that path rebuilds the Windows env block and would drop `SPT_HOME`). The\n/// broker stamps `{generation, start-reason}` onto the argv (ADR-0018 V2).\n///\n/// `binary` is the **record-driven selection** (D6-2): `None` spawns the\n/// accepted / candidate binary (`canonical`, below), `Some(path)` spawns the\n/// rollback binary (the `.old-N` last-known-good) — the supervisor chooses, no\n/// file is renamed at the failure instant.\n///\n/// `canonical` is the canonical exe path captured ONCE at broker start\n/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n/// record-driven `.old-N` selection); else the `canonical` path captured at\n/// broker start; else — only if that capture failed — degrade to a per-spawn\n/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n/// when a canonical path is available, because on Linux `current_exe()` follows\n/// the `apply` rename to the old binary.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n    match binary {\n        Some(p) => Ok(p.to_path_buf()),\n        None => match canonical {\n            Some(c) => Ok(c.to_path_buf()),\n            None => std::env::current_exe(),\n        },\n    }\n}\n\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn spawn_brain_child(\n    generation: u64,\n    reason: StartReason,\n    binary: Option<&Path>,\n    canonical: Option<&Path>,\n) -> io::Result<Child> {\n    let exe = select_brain_exe(binary, canonical)?;\n    let mut cmd = Command::new(exe);\n    cmd.args(brain_child_args(generation, reason));\n    // The brain is a background logic process — it must never flash a console\n    // window. On Windows a plain console-subsystem spawn pops one whenever the\n    // broker has a console (a `spt daemon run` in a terminal, or any test that\n    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n    // the plain-child SPT_HOME inheritance (above) is preserved.\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n        cmd.creation_flags(CREATE_NO_WINDOW);\n    }\n    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n    // OWN process group so the daemon can reap the brain + everything it spawns\n    // (the shellwake watchers, the detached Psyches — all inherit this group) as\n    // one unit on stop, without signalling the daemon itself. Windows roots the\n    // reap on a Job object instead (assigned in the supervisor via BrainReaper).\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        unsafe {\n            cmd.pre_exec(|| {\n                // setpgid(0, 0): this child becomes the leader of a new group\n                // (pgid == its pid). Best-effort — a failure only loses the reap.\n                libc::setpgid(0, 0);\n                Ok(())\n            });\n        }\n    }\n    cmd.spawn()\n}\n\n/// The versioned brain-spawn argv (KH-2.3): `daemon brain --generation <N>\n/// --start-reason <reason>`. Both flags are **additive + defaulted** on the\n/// brain entry, so an older broker that spawns this brain with the *bare* old\n/// argv (no flags) still parses — the brain falls back to generation 0 /"}],"details":{"displayContent":{"text":"//! The brain **process** + its broker-side supervisor (ADR-0018 Q2/Q3 — the\n//! broker/brain process-isolation restoration, D1 skeleton).\n//!\n//! ADR-0004 specified the broker/brain split as two **processes**; production\n//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n//! the binary on disk but never restarts the running code (the regression\n//! ADR-0018 corrects). D1 restores the process boundary as a *true minimal\n//! skeleton*: `spt daemon run` is the broker process and spawns a supervised\n//! `spt daemon brain` child. The child connects over the existing broker socket\n//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n//! verb. The digest hub serve + seed-control stay broker-side continuity\n//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n//! loops.\n//!\n//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n//! broker spawns + supervises a brain child over the socket, and the broker\n//! (with its PTY children + seed channel) survives the brain dying — the\n//! supervisor respawns it from the executable path, which on a real update is\n//! the swapped binary.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\nuse std::io;\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::endpoint::{brain_ready_path, broker_socket_name};\nuse crate::relcache::{AppliedRecord, ReleaseCache};\n\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n/// never a hot respawn loop.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n/// A run at least this long counts as healthy and resets the backoff floor.\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n/// brain has this long to signal `ready` for its generation before the window\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n/// **exits before** signaling ready is respawned this many consecutive times\n/// (a transient first-boot hiccup should not abandon a good binary) before the\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n/// window-expiry triggers rollback.\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// How long the brain child retries connecting to the broker socket before\n/// giving up (the broker may still be binding — the child is freshly spawned).\nconst BRAIN_CONNECT_TRIES: u32 = 300;\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\n/// reliable than a brain→brain frame, which retires in D4).\n///\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\n/// never a parse failure that would brick the brain before it rehydrates.\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum StartReason {\n    /// The broker's first brain spawn this broker lifetime.\n    Cold,\n    /// A supervised respawn after an unexpected brain exit.\n    Crash,\n    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).\n    Update,\n}\n\nimpl StartReason {\n    /// The argv token the broker passes to `spt daemon brain --start-reason`.\n    pub fn as_arg(self) -> &'static str {\n        match self {\n            StartReason::Cold => \"cold\",\n            StartReason::Crash => \"crash\",\n            StartReason::Update => \"update\",\n        }\n    }\n\n    /// Parse the `--start-reason` argv value, lenient: an unknown token (a\n    /// newer broker's reason an older brain predates) defaults to `Cold` rather\n    /// than failing the parse (KH-2.3 — defaults survive intermediate versions).\n    pub fn from_arg(s: &str) -> Self {\n        match s {\n            \"update\" => StartReason::Update,\n            \"crash\" => StartReason::Crash,\n            _ => StartReason::Cold,\n        }\n    }\n}\n\n/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n/// its next wait tick, kills the current brain child, and respawns it with\n/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n/// between the broker serve path and the supervisor — both run in the broker\n/// process. This is the seam that makes a routine `apply` actually run new code,\n/// the pillar the in-process-collapse regression silently unrealized.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n#[derive(Default)]\npub struct BrainRestart {\n    requested: AtomicBool,\n}\n\nimpl BrainRestart {\n    /// A fresh signal with no pending request.\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Request a planned restart (broker handler side) — idempotent: coalesces\n    /// with any already-pending request into one cycle.\n    pub fn request(&self) {\n        self.requested.store(true, Ordering::SeqCst);\n    }\n\n    /// Consume a pending request (supervisor side): true at most once per\n    /// `request`, clearing it so the next cycle starts fresh.\n    pub fn take(&self) -> bool {\n        self.requested.swap(false, Ordering::SeqCst)\n    }\n}\n\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n/// holding the connection and probing broker liveness each heartbeat. A dropped\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n/// from the (possibly swapped) binary.\n///\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n…\nenum TrialStep {\n…\n    /// The caller MUST kill it before spawning the rollback binary (**A11** — never\n    /// two live brains).\n    WindowElapsedAlive,\n    /// `stop` was raised during the trial — the caller kills + returns.\n    Stopped,\n}\n\n/// Watch a freshly-spawned trial candidate for readiness within `window`\n/// (ADR-0018 Q7, D6-2). Polls in `TICK` slices (so a daemon stop stays prompt,\n/// A11/note-ii) for a `brain.ready` stamp **== this spawn's `generation`** — a\n/// stale stamp from another generation never satisfies the gate (the single\n/// load-bearing correctness invariant; the A9 clear-before-spawn closes the\n/// cross-lifetime exact-generation hole). Returns the moment readiness is seen\n/// **AND the old generation has drained**, the child exits pre-ready, the window\n/// elapses, or `stop` is raised.\n///\n/// **The DRAINED precondition (REQ-UPDATE-PROMOTE-DRAINED).** `brain.ready` alone is\n/// NOT a promote: the W2 stall-evict only BOUNDS the false-promote window to\n/// `BRAIN_WRITE_DEADLINE`, so a new brain can signal ready while the OLD generation's\n/// local controller conn is still wedged (the 22:47 apply \"promoted\" onto a frozen\n/// control plane). So readiness is LATCHED (`ready_seen` — a candidate that stamped\n/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n/// the candidate and rolls back (conservative: never a false-promote onto a wedge).\n/// The normal case drains well inside the window — W2 bounds a real drain to\n/// `BRAIN_WRITE_DEADLINE` (15 s) < [`SUPERVISE_HEALTHY_RUN`] (30 s) — and a cleanly-\n/// closed old conn drains at once. `child.try_wait()` is consulted only while\n/// NOT-yet-ready, so a ready-then-exited candidate still promotes once drained.\nfn run_trial(\n    child: &mut Child,\n    generation: u64,\n    window: Duration,\n    env: &dyn TrialEnv,\n    stop: &AtomicBool,\n) -> TrialStep {\n    let deadline = Instant::now() + window;\n    let mut ready_seen = false;\n    loop {\n        if stop.load(Ordering::Relaxed) {\n            return TrialStep::Stopped;\n        }\n        // Latch readiness FIRST (don't demote a ready-then-exit child) — but do NOT\n        // promote on it alone: the old generation's control plane must have drained.\n        if !ready_seen && env.ready_generation() == Some(generation) {\n            ready_seen = true;\n        }\n        if ready_seen && env.old_gen_drained() {\n            return TrialStep::Promoted;\n        }\n        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n        // once ready-seen we are waiting on the OLD gen to drain, and a ready-then-\n        // exited child still promotes (the exit is a normal later respawn).\n        if !ready_seen {\n            match child.try_wait() {\n                Ok(Some(_)) => return TrialStep::PreReadyExit,\n                Ok(None) => {}\n                // A wait error: treat as a pre-ready exit (fail toward retry, never\n                // a panic in the supervisor thread).\n                Err(e) => {\n                    eprintln!(\"BRAIN_TRIAL_WAIT_FAIL: {e}\");\n                    return TrialStep::PreReadyExit;\n                }\n            }\n        }\n        if Instant::now() >= deadline {\n            return TrialStep::WindowElapsedAlive;\n        }\n        thread::sleep(TICK);\n    }\n}\n\n/// Supervise the brain child: spawn it, wait for it, and respawn on exit after a\n/// capped, doubling backoff — until `stop` is raised, which kills the live child\n/// and returns. The process-level sibling of [`crate::pump::supervise_pump`]\n/// (ADR-0018 Q2: the broker is the always-up anchor that respawns the brain;\n/// Q3: a planned update is just the brain self-exiting on a path the supervisor\n/// already recovers from).\n///\n/// **Readiness-gated promotion + auto-rollback (ADR-0018 Q7, D6-2).** When the\n/// injected `env` reports an unpromoted `AppliedPending` record, the spawn is a\n/// **trial** (the latch is the *record*, **A8** — regardless of start reason):\n/// the supervisor clears `brain.ready` (**A9**), spawns the candidate, and waits\n/// for it to stamp ready for ITS generation within `window`. It **promotes** on\n/// ready (writes `Applied`, the candidate is now the accepted binary and a later\n/// crash is a normal respawn); it **rolls back** to the last-known-good binary\n/// on budget exhaustion — [`TRIAL_PREREADY_BUDGET`] consecutive pre-ready exits\n/// **or** one alive-but-never-ready window-expiry (which **kills** the candidate\n/// first, **A11** — never two live brains). Binary selection is **record-driven**\n/// (a `RolledBack` record spawns the good `.old-N` binary, surviving a reboot for\n/// free), so no file is renamed at the failure instant. Every hosted endpoint is\n/// untouched throughout — the broker holds them across the whole cycle.\n///\n/// Generic over `spawn_child` (which receives the **selected binary** — `None` =\n/// the default `current_exe`, `Some(path)` = the rollback binary) and `env`, so\n/// the unit harness injects fakes; production binds [`spawn_brain_child`] +\n/// [`ProductionTrialEnv`].\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn supervise_brain(\n    stop: &AtomicBool,\n    restart: &BrainRestart,\n    base: Duration,\n    env: &dyn TrialEnv,\n    window: Duration,\n    mut spawn_child: impl FnMut(u64, StartReason, Option<&Path>) -> io::Result<Child>,\n) {\n    let mut backoff = base;\n    // Broker-owned generation custody (ADR-0018 V2, KH-2.4): the counter lives in\n    // the never-restarting supervisor and increments on EVERY spawn (cold or\n    // respawn), so the broker — which observes every respawn — is the\n    // authoritative source, not the outgoing brain. The first spawn is `Cold`;\n    // every supervised respawn here is a `Crash` (an unexpected exit the\n    // supervisor recovers from). A *planned* update respawn stamps `Update`\n    // instead — wired in D3-3, where the apply trigger drives this loop.\n    let mut generation: u64 = 0;\n    let mut reason = StartReason::Cold;\n    // Consecutive pre-ready exits of the CURRENT trial candidate (A8 budget K).\n    // In-memory: a broker restart resets it — acceptable, the trial restarts.\n    let mut pre_ready_exits: u32 = 0;\n    while !stop.load(Ordering::Relaxed) {\n        let started = Instant::now();\n        // Record-driven binary selection + trial latch (A8). The latch is the\n        // unpromoted `AppliedPending` record, NOT the start reason; a `RolledBack`\n        // record selects the last-known-good binary (and is never a trial — it is\n        // the recovery steady state). A missing/corrupt record degrades to \"no\n        // trial, default binary\" — the supervisor never panics on the record.\n        let record = env.applied_state();\n        let binary: Option<PathBuf> = match &record {\n            Some(AppliedRecord::RolledBack { rollback_binary, .. }) => {\n                Some(PathBuf::from(rollback_binary))\n            }\n            _ => None,\n        };\n        let is_trial = matches!(record, Some(AppliedRecord::AppliedPending { .. }));\n        // A9: clear `brain.ready` immediately before a trial spawn — the prior\n        // child is already waited-on dead (below), so the only stamp that can\n        // exist post-clear is THIS trial child's. Closes the cross-lifetime hole\n        // that same-generation equality alone cannot (a stale gen-N file from a\n        // pre-reboot lifetime matching a fresh gen-N trial).\n        if is_trial {\n            env.clear_ready();\n        }\n        // Did this cycle end because of a planned update restart (Q3) rather than\n        // an unexpected crash? The broker — observing the cycle it initiated — is\n        // the authoritative source of that distinction (V2).\n        let mut planned = false;\n        match spawn_child(generation, reason, binary.as_deref()) {\n            Ok(mut child) => {\n                // The trial gate runs first for a candidate on trial; on\n                // promotion it falls through to the normal wait loop for the SAME\n                // child. A non-trial spawn skips straight to normal supervision.\n                let mut supervise = !is_trial;\n                if is_trial {\n                    match run_trial(&mut child, generation, window, env, stop) {\n                        TrialStep::Stopped => {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        TrialStep::Promoted => {\n                            // Bytes-gate (KH 6.11): readiness != new-bytes. Promote\n                            // only if the candidate is running the staged bytes; a\n                            // ready-but-wrong-bytes candidate (e.g. a Linux respawn\n                            // onto the renamed old binary) fails the trial → kill +\n                            // rollback, never a falsely-`applied` record.\n                            let version = match &record {\n                                Some(AppliedRecord::AppliedPending { version, .. }) => Some(*version),\n                                _ => None,\n                            };\n                            if let Some(version) = version {\n                                if let BytesGate::Mismatch = bytes_gate(env, version) {\n                                    eprintln!(\"BRAIN_PROMOTE_REJECTED: candidate v{version} ready but running the WRONG bytes (generation {generation}) — rolling back (KH 6.11)\");\n                                    let _ = child.kill();\n                                    let _ = child.wait();\n                                    rollback(env, &record);\n                                    pre_ready_exits = 0;\n                                    generation += 1;\n                                    reason = StartReason::Crash;\n                                    backoff = base;\n                                    continue;\n                                }\n                                eprintln!(\"BRAIN_PROMOTED: candidate v{version} signalled ready (generation {generation}) — swap accepted\");\n                                env.record_promoted(version);\n                            }\n                            pre_ready_exits = 0;\n                            backoff = base;\n                            supervise = true; // keep supervising the now-accepted child\n                        }\n                        TrialStep::PreReadyExit => {\n                            pre_ready_exits += 1;\n                            eprintln!(\"BRAIN_TRIAL_EXIT: candidate exited pre-ready ({pre_ready_exits}/{TRIAL_PREREADY_BUDGET})\");\n                            if pre_ready_exits >= TRIAL_PREREADY_BUDGET {\n                                rollback(env, &record);\n                                pre_ready_exits = 0;\n                                // The rollback binary is known-good — respawn it\n                                // promptly, no crash-penalty backoff (mirror the\n                                // WindowElapsedAlive path); only the under-budget\n                                // candidate retries keep the doubling backoff.\n                                backoff = base;\n                            } else {\n                                backoff = next_backoff(backoff, started.elapsed(), base);\n                            }\n                            generation += 1;\n                            reason = StartReason::Crash;\n                            sleep_backoff(backoff, stop);\n                            continue;\n                        }\n                        TrialStep::WindowElapsedAlive => {\n                            // A11: KILL the alive-but-never-ready candidate BEFORE\n                            // spawning the rollback binary — two live brains would\n                            // break every single-brain seed/socket/IPC assumption.\n                            eprintln!(\"BRAIN_TRIAL_TIMEOUT: candidate alive but never ready — killing before rollback\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            rollback(env, &record);\n…\n                        }\n                    }\n                }\n…\n            }\n…\n        }\n…\n        if planned {\n…\n            backoff = base;\n            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n            continue;\n        }\n        reason = StartReason::Crash;\n        backoff = next_backoff(backoff, started.elapsed(), base);\n        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n        sleep_backoff(backoff, stop);\n    }\n}\n\n/// Effect the rollback for an exhausted trial (D6-2): from the candidate's\n/// `AppliedPending` record, write `RolledBack{quarantine=N, running, rollback_binary}`\n/// and fire the loud notif (via `env`). `running` is the candidate's\n/// `prior_version` (the version actually running before the swap, captured by\n/// `apply`) — NOT `N-1`, which is wrong on a skip-version update (applied v5 →\n/// candidate v8: the `.old-8` aside holds v5 bytes, so `running` is v5, not v7).\n/// It degrades to `N-1` only for a pre-D6-2b record that lacks `prior_version`.\n/// A record that is not `AppliedPending` (already rolled back, or raced away) is\n/// a no-op — never a double rollback.\n///\n/// **Rollback is SELECTION, not an update (open-call-4).** The recovery never\n/// routes through the update engine / `apply` / `verify_metadata`: it just makes\n/// the supervisor *spawn the `.old-N` binary*. So the anti-downgrade monotonic\n/// version floor (`REQ-HAZARD-UPDATE-ROLLBACK` — *attack* protection that refuses\n/// a version *downgrade*) **never sees** this recovery, and the two concerns stay\n/// cleanly separate. Do NOT \"fix\" `verify_metadata` to permit recovery\n/// downgrades — selection-not-apply is the mechanism, by design.\nfn rollback(env: &dyn TrialEnv, record: &Option<AppliedRecord>) {\n    if let Some(AppliedRecord::AppliedPending {\n        version,\n        rollback_binary,\n        prior_version,\n        ..\n    }) = record\n    {\n        let running = prior_version.unwrap_or(version.saturating_sub(1));\n        eprintln!(\"BRAIN_ROLLBACK: update v{version} failed readiness — rolling back to v{running} ({rollback_binary}), quarantining v{version}\");\n        env.record_rolled_back(*version, running, rollback_binary);\n    }\n}\n\n/// Promotion bytes-gate verdict (KH 6.11, `REQ-HAZARD-BRAIN-RESPAWN-PATH`).\nenum BytesGate {\n    /// Candidate is running the staged bytes — promote.\n    Match,\n    /// Candidate is ready but running the WRONG bytes — fail the trial, roll back.\n    Mismatch,\n    /// Either hash absent (pre-metadata release / missing breadcrumb) — degrade\n    /// to readiness-only promotion (N-1-safe).\n    Unverified,\n}\n\n/// Compare the just-ready candidate's stamped `exe_hash` against the staged\n/// artifact hash for `version` on this platform (KH 6.11). Readiness alone is\n/// NOT proof the new bytes run — on Linux a brain respawned onto the renamed\n/// old binary still signals ready (the v0.4.1 kitsubito false-success). On\n/// either hash absent the gate degrades to readiness-only but emits\n/// `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n        (Some(_), Some(_)) => BytesGate::Mismatch,\n        (expected, _) => {\n            let which = if expected.is_none() {\n                \"artifact-hash-absent\"\n            } else {\n                \"ready-hash-absent\"\n            };\n            eprintln!(\"PROMOTE_BYTES_UNVERIFIED:{version}:{which}\");\n            BytesGate::Unverified\n        }\n    }\n}\n\n/// Spawn the production brain child: re-exec `daemon brain` as a plain\n/// (non-detached) managed child, so it inherits the broker's environment —\n/// including `SPT_HOME` — and dies with no orphaning. NOT `spawn_detached`\n/// (that path rebuilds the Windows env block and would drop `SPT_HOME`). The\n/// broker stamps `{generation, start-reason}` onto the argv (ADR-0018 V2).\n///\n/// `binary` is the **record-driven selection** (D6-2): `None` spawns the\n/// accepted / candidate binary (`canonical`, below), `Some(path)` spawns the\n/// rollback binary (the `.old-N` last-known-good) — the supervisor chooses, no\n/// file is renamed at the failure instant.\n///\n/// `canonical` is the canonical exe path captured ONCE at broker start\n/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n/// record-driven `.old-N` selection); else the `canonical` path captured at\n/// broker start; else — only if that capture failed — degrade to a per-spawn\n/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n/// when a canonical path is available, because on Linux `current_exe()` follows\n/// the `apply` rename to the old binary.\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n    match binary {\n        Some(p) => Ok(p.to_path_buf()),\n        None => match canonical {\n            Some(c) => Ok(c.to_path_buf()),\n            None => std::env::current_exe(),\n        },\n    }\n}\n\n// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\nfn spawn_brain_child(\n    generation: u64,\n    reason: StartReason,\n    binary: Option<&Path>,\n    canonical: Option<&Path>,\n) -> io::Result<Child> {\n    let exe = select_brain_exe(binary, canonical)?;\n    let mut cmd = Command::new(exe);\n    cmd.args(brain_child_args(generation, reason));\n    // The brain is a background logic process — it must never flash a console\n    // window. On Windows a plain console-subsystem spawn pops one whenever the\n    // broker has a console (a `spt daemon run` in a terminal, or any test that\n    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n    // the plain-child SPT_HOME inheritance (above) is preserved.\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n        cmd.creation_flags(CREATE_NO_WINDOW);\n    }\n    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n    // OWN process group so the daemon can reap the brain + everything it spawns\n    // (the shellwake watchers, the detached Psyches — all inherit this group) as\n    // one unit on stop, without signalling the daemon itself. Windows roots the\n    // reap on a Job object instead (assigned in the supervisor via BrainReaper).\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        unsafe {\n            cmd.pre_exec(|| {\n                // setpgid(0, 0): this child becomes the leader of a new group\n                // (pgid == its pid). Best-effort — a failure only loses the reap.\n                libc::setpgid(0, 0);\n                Ok(())\n            });\n        }\n    }\n    cmd.spawn()\n}\n\n/// The versioned brain-spawn argv (KH-2.3): `daemon brain --generation <N>\n/// --start-reason <reason>`. Both flags are **additive + defaulted** on the\n/// brain entry, so an older broker that spawns this brain with the *bare* old\n/// argv (no flags) still parses — the brain falls back to generation 0 /","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,null,639,null,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,null,873,874,875,null,907,null,909,null,918,null,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"}}},"isError":false,"timestamp":1784195829777}}
{"type":"message","id":"b3ce29b1","parentId":"c94329bd","timestamp":"2026-07-16T09:57:09.778Z","message":{"role":"toolResult","toolCallId":"call_PinQNhsEnr0sa8AuC6xQ3YA1|fc_0698befac979932d016a58aaf53144819aa1079beecc6a1b8c","toolName":"read","content":[{"type":"text","text":"enum DescriptionCmd {\n…\n    Set {\n…\n    },\n    /// Show a local endpoint's authored blurb (the bare `description` view).\n    Show {\n        /// The local endpoint id (auto-detected if omitted).\n        id: Option<String>,\n    },\n}\n\n/// `spt daemon <action>` subcommands (M8 decisions 5, 23, 25b — REQ-CLI-2).\n// [impl->REQ-CLI-2]\n#[derive(Subcommand)]\nenum DaemonCmd {\n    /// Run the per-machine daemon in the FOREGROUND — this process IS the\n    /// daemon, blocking until signalled (the service unit's ExecStart, or\n    /// manual debugging). Never detaches; for a background daemon use `start`.\n    Run {\n        /// Hidden (REQ-HAZARD-WMI-DAEMON-WINDOW): set by the auto-start LADDER on\n        /// every rung so the broker FreeConsole()s its own console at startup (the\n        /// WMI/schtasks `start /b` rungs otherwise allocate a conhost window).\n        /// Absent on a human-typed `spt daemon run` → foreground console preserved.\n        #[arg(long, hide = true)]\n        detached: bool,\n    },\n    /// Ensure the daemon is up in the background (idempotent, service-aware):\n    /// a registered OS service is driven via its manager, else a detached\n    /// daemon is spawned. Non-blocking.\n    Start,\n    /// Stop the daemon (service-aware: a managed service is stopped via its\n    /// manager so it does not auto-restart-fight; else a graceful IPC stop).\n    /// Refuses with a warning if it hosts live sessions (they would be killed) —\n    /// pass --force to stop anyway.\n    Stop {\n        /// Stop even when the daemon hosts live sessions (which the stop kills).\n        /// Without it, a daemon with live hosted sessions refuses and names them.\n        #[arg(long)]\n        force: bool,\n    },\n    /// Node status: daemon state, member subnets, local endpoints (the\n    /// bare `spt daemon` view).\n    Status,\n    /// Restart the daemon's coordinator process in place — no binary change,\n    /// no stop/start. Hosted terminals and the network layer keep running\n    /// untouched; only the coordinator cycles. The recovery verb for a stuck\n    /// coordinator (e.g. endpoint bringup wedged) that previously needed a\n    /// full `daemon stop`/`start` killing every hosted session.\n    Refresh,\n    /// Hidden (ADR-0018 D1): the brain child process. The broker (`daemon run`)\n    /// spawns + supervises this; it connects to the broker over the local IPC,\n    /// signals ready, and idles. Not for direct user invocation.\n    ///\n    /// The broker stamps `--generation`/`--start-reason` at spawn (ADR-0018 V2\n    /// generation custody + the Q4 update-vs-crash discriminator). Both are\n    /// **defaulted** so an older broker that spawns this brain with the bare old\n    /// argv still parses (generation 0 / `cold`) — the new-brain × old-broker\n    /// N-1 steady state (KH-2.3 handoff-argv-compat).\n    #[command(hide = true)]\n…\n}\n…\npub fn run() -> i32 {\n…\n    match command {\n…\n        Cmd::Notif { action } => match action {\n            NotifCmd::List { subnet } => cmd_notif_list(subnet, json),\n            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),\n        },\n        Cmd::Update {\n            action,\n            core_only,\n            restart,\n        } => match action {\n            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n            Some(UpdateCmd::Fetch {\n                channel,\n                tag,\n                apply,\n            }) => cmd_update_fetch(channel, tag, apply),\n            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n            Some(UpdateCmd::Adapters { names }) => {\n                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n            }\n            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n            None => cmd_update_composite(core_only, restart),\n        },\n        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n        Cmd::Install { dir, no_path } => cmd_install(dir, no_path),\n        // [impl->REQ-DOCS-LOCAL-SERVER]\n        Cmd::Docs { action } => cmd_docs(action),\n        Cmd::Api(args) => crate::api::run(args),\n        Cmd::Spt => {\n            println!(\"Sentience Pocket Transacter\");\n            0\n        }\n        // [impl->REQ-SPOOL-TAKE-AUDIT] debug read of a perch's spool taker-audit.\n        Cmd::SpoolAudit { id, json } => cmd_spool_audit(&id, json),\n        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.\n        // [impl->REQ-CLI-2]\n        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {\n            DaemonCmd::Run { detached } => cmd_daemon_run(detached),\n            DaemonCmd::Start => cmd_daemon_start(),\n            DaemonCmd::Stop { force } => cmd_daemon_stop(force),\n            DaemonCmd::Status => cmd_daemon_status(json),\n            // [impl->REQ-DAEMON-REFRESH]\n            DaemonCmd::Refresh => cmd_daemon_refresh(),\n            DaemonCmd::Brain {\n                generation,\n                start_reason,\n            } => cmd_daemon_brain(generation, &start_reason),\n        },\n    }\n}\n\n/// Debug read of a perch's spool taker-audit (W5, REQ-SPOOL-TAKE-AUDIT): every retained\n/// row with WHO took it — the leg, the taker's sid + pid, and the take wall-ms — so an\n/// incident RCA can answer \"who marked this row delivered\" in seconds, not an hour.\n/// `--json` emits the rows verbatim; the human view is a compact table. Non-destructive.\n// [impl->REQ-SPOOL-TAKE-AUDIT]\n…\n/// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The broker\n/// process (`daemon run`) spawns + supervises this; it is not a user-facing\n/// entry. FOREGROUND: this process IS the brain, blocking until the broker\n/// connection drops (then it exits for a supervised respawn). `generation` +\n/// `start_reason` are the broker's spawn-time stamp (ADR-0018 V2), defaulted so\n/// an older broker's bare argv still parses (KH-2.3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn cmd_daemon_brain(generation: u64, start_reason: &str) -> i32 {\n    match spt_daemon::Daemon::run_brain(generation, start_reason) {\n        Ok(()) => 0,\n        Err(e) => {\n            eprintln!(\"BRAIN_EXIT: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon start` (REQ-DAEMON-6) — ensure the daemon is up in the\n/// background, idempotent + service-aware: a registered OS service is driven\n/// via its manager (never a competing manual daemon), else a detached spawn.\n// [impl->REQ-DAEMON-6]\nfn cmd_daemon_start() -> i32 {\n    match spt_daemon::start_daemon() {\n        Ok(spt_daemon::StartOutcome::AlreadyRunning) => {\n            eprintln!(\"DAEMON_ALREADY_RUNNING\");\n            0\n        }\n        Ok(spt_daemon::StartOutcome::ServiceStarted(label)) => {\n            eprintln!(\"DAEMON_STARTED (via {label})\");\n            0\n        }\n        Ok(spt_daemon::StartOutcome::Spawned) => {\n            eprintln!(\"DAEMON_STARTED\");\n            0\n        }\n        Err(e) => {\n            eprintln!(\"DAEMON_START_FAIL: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon stop` — graceful, service-aware shutdown (REQ-DAEMON-6, M8\n/// decision 5 / REQ-CLI-2): a managed service is stopped through its manager\n/// (so it does not auto-restart-fight); a manual daemon acks the seed-control\n/// stop then exits. Idempotent UX: not-running is already-stopped (exit 0).\n/// The live-session stop guard (REQ-DAEMON-STOP-LIVE-SESSION-WARN): pure — a daemon\n/// hosting live sessions REFUSES to stop (naming them) unless `--force`, so a stop\n/// never silently kills hosted endpoints out from under an operator. Empty set (no\n/// hosted sessions) or `--force` ⇒ proceed. `Err` carries the operator warning.\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn stop_live_session_guard(live_sessions: &[String], force: bool) -> Result<(), String> {\n    if live_sessions.is_empty() || force {\n        return Ok(());\n    }\n    Err(format!(\n        \"the daemon is hosting {} live session(s) that stopping would kill: {}. \\\n         Re-run with `spt daemon stop --force` to stop anyway — they come back on the \\\n         next `spt daemon start` — or leave the daemon running.\",\n        live_sessions.len(),\n        live_sessions.join(\", \")\n    ))\n}\n\n/// The endpoint ids the running daemon currently hosts a broker PTY session for —\n/// the stop-guard's \"would be killed\" set. Best-effort: a down/unreachable daemon\n/// yields none (the guard is then a no-op and the normal stop path reports\n/// already-stopped). // [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn live_hosted_session_ids() -> Vec<String> {\n    if !spt_daemon::is_running() {\n        return Vec::new();\n    }\n    match spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms()) {\n        Ok(mut b) => b\n            .sessions()\n            .map(|r| {\n                r.sessions\n                    .into_iter()\n                    .map(|s| s.endpoint)\n                    .filter(|e| !e.is_empty())\n                    .collect()\n            })\n            .unwrap_or_default(),\n        Err(_) => Vec::new(),\n    }\n}\n\n// [impl->REQ-DAEMON-6]\n// [impl->REQ-CLI-2]\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn cmd_daemon_stop(force: bool) -> i32 {\n    // Guard: refuse + name the sessions if stopping would kill live hosted endpoints,\n    // unless --force. Runs BEFORE the stop so nothing is torn down on a refusal.\n    let live = live_hosted_session_ids();\n    if let Err(msg) = stop_live_session_guard(&live, force) {\n        eprintln!(\"DAEMON_STOP_REFUSED: {msg}\");\n        return 3;\n    }\n    match spt_daemon::stop_daemon() {\n        Ok(spt_daemon::StopOutcome::NotRunning) => {\n            eprintln!(\"DAEMON_NOT_RUNNING (already stopped)\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::ServiceStopped(label)) => {\n            eprintln!(\"DAEMON_STOPPED (via {label})\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::IpcStopped) => {\n            eprintln!(\"DAEMON_STOPPED\");\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)\" +"}],"details":{"displayContent":{"text":"enum DescriptionCmd {\n…\n    Set {\n…\n    },\n    /// Show a local endpoint's authored blurb (the bare `description` view).\n    Show {\n        /// The local endpoint id (auto-detected if omitted).\n        id: Option<String>,\n    },\n}\n\n/// `spt daemon <action>` subcommands (M8 decisions 5, 23, 25b — REQ-CLI-2).\n// [impl->REQ-CLI-2]\n#[derive(Subcommand)]\nenum DaemonCmd {\n    /// Run the per-machine daemon in the FOREGROUND — this process IS the\n    /// daemon, blocking until signalled (the service unit's ExecStart, or\n    /// manual debugging). Never detaches; for a background daemon use `start`.\n    Run {\n        /// Hidden (REQ-HAZARD-WMI-DAEMON-WINDOW): set by the auto-start LADDER on\n        /// every rung so the broker FreeConsole()s its own console at startup (the\n        /// WMI/schtasks `start /b` rungs otherwise allocate a conhost window).\n        /// Absent on a human-typed `spt daemon run` → foreground console preserved.\n        #[arg(long, hide = true)]\n        detached: bool,\n    },\n    /// Ensure the daemon is up in the background (idempotent, service-aware):\n    /// a registered OS service is driven via its manager, else a detached\n    /// daemon is spawned. Non-blocking.\n    Start,\n    /// Stop the daemon (service-aware: a managed service is stopped via its\n    /// manager so it does not auto-restart-fight; else a graceful IPC stop).\n    /// Refuses with a warning if it hosts live sessions (they would be killed) —\n    /// pass --force to stop anyway.\n    Stop {\n        /// Stop even when the daemon hosts live sessions (which the stop kills).\n        /// Without it, a daemon with live hosted sessions refuses and names them.\n        #[arg(long)]\n        force: bool,\n    },\n    /// Node status: daemon state, member subnets, local endpoints (the\n    /// bare `spt daemon` view).\n    Status,\n    /// Restart the daemon's coordinator process in place — no binary change,\n    /// no stop/start. Hosted terminals and the network layer keep running\n    /// untouched; only the coordinator cycles. The recovery verb for a stuck\n    /// coordinator (e.g. endpoint bringup wedged) that previously needed a\n    /// full `daemon stop`/`start` killing every hosted session.\n    Refresh,\n    /// Hidden (ADR-0018 D1): the brain child process. The broker (`daemon run`)\n    /// spawns + supervises this; it connects to the broker over the local IPC,\n    /// signals ready, and idles. Not for direct user invocation.\n    ///\n    /// The broker stamps `--generation`/`--start-reason` at spawn (ADR-0018 V2\n    /// generation custody + the Q4 update-vs-crash discriminator). Both are\n    /// **defaulted** so an older broker that spawns this brain with the bare old\n    /// argv still parses (generation 0 / `cold`) — the new-brain × old-broker\n    /// N-1 steady state (KH-2.3 handoff-argv-compat).\n    #[command(hide = true)]\n…\n}\n…\npub fn run() -> i32 {\n…\n    match command {\n…\n        Cmd::Notif { action } => match action {\n            NotifCmd::List { subnet } => cmd_notif_list(subnet, json),\n            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),\n        },\n        Cmd::Update {\n            action,\n            core_only,\n            restart,\n        } => match action {\n            Some(UpdateCmd::Apply { finish }) => cmd_update_apply(finish),\n            Some(UpdateCmd::Fetch {\n                channel,\n                tag,\n                apply,\n            }) => cmd_update_fetch(channel, tag, apply),\n            // [impl->REQ-UPDATE-ADAPTERS-VERB] the alias form; one engine.\n            Some(UpdateCmd::Adapters { names }) => {\n                cmd_adapter_update(&spt_store::perch::adapters_dir(), names.as_deref())\n            }\n            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n            None => cmd_update_composite(core_only, restart),\n        },\n        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n        Cmd::Install { dir, no_path } => cmd_install(dir, no_path),\n        // [impl->REQ-DOCS-LOCAL-SERVER]\n        Cmd::Docs { action } => cmd_docs(action),\n        Cmd::Api(args) => crate::api::run(args),\n        Cmd::Spt => {\n            println!(\"Sentience Pocket Transacter\");\n            0\n        }\n        // [impl->REQ-SPOOL-TAKE-AUDIT] debug read of a perch's spool taker-audit.\n        Cmd::SpoolAudit { id, json } => cmd_spool_audit(&id, json),\n        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.\n        // [impl->REQ-CLI-2]\n        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {\n            DaemonCmd::Run { detached } => cmd_daemon_run(detached),\n            DaemonCmd::Start => cmd_daemon_start(),\n            DaemonCmd::Stop { force } => cmd_daemon_stop(force),\n            DaemonCmd::Status => cmd_daemon_status(json),\n            // [impl->REQ-DAEMON-REFRESH]\n            DaemonCmd::Refresh => cmd_daemon_refresh(),\n            DaemonCmd::Brain {\n                generation,\n                start_reason,\n            } => cmd_daemon_brain(generation, &start_reason),\n        },\n    }\n}\n\n/// Debug read of a perch's spool taker-audit (W5, REQ-SPOOL-TAKE-AUDIT): every retained\n/// row with WHO took it — the leg, the taker's sid + pid, and the take wall-ms — so an\n/// incident RCA can answer \"who marked this row delivered\" in seconds, not an hour.\n/// `--json` emits the rows verbatim; the human view is a compact table. Non-destructive.\n// [impl->REQ-SPOOL-TAKE-AUDIT]\n…\n/// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The broker\n/// process (`daemon run`) spawns + supervises this; it is not a user-facing\n/// entry. FOREGROUND: this process IS the brain, blocking until the broker\n/// connection drops (then it exits for a supervised respawn). `generation` +\n/// `start_reason` are the broker's spawn-time stamp (ADR-0018 V2), defaulted so\n/// an older broker's bare argv still parses (KH-2.3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn cmd_daemon_brain(generation: u64, start_reason: &str) -> i32 {\n    match spt_daemon::Daemon::run_brain(generation, start_reason) {\n        Ok(()) => 0,\n        Err(e) => {\n            eprintln!(\"BRAIN_EXIT: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon start` (REQ-DAEMON-6) — ensure the daemon is up in the\n/// background, idempotent + service-aware: a registered OS service is driven\n/// via its manager (never a competing manual daemon), else a detached spawn.\n// [impl->REQ-DAEMON-6]\nfn cmd_daemon_start() -> i32 {\n    match spt_daemon::start_daemon() {\n        Ok(spt_daemon::StartOutcome::AlreadyRunning) => {\n            eprintln!(\"DAEMON_ALREADY_RUNNING\");\n            0\n        }\n        Ok(spt_daemon::StartOutcome::ServiceStarted(label)) => {\n            eprintln!(\"DAEMON_STARTED (via {label})\");\n            0\n        }\n        Ok(spt_daemon::StartOutcome::Spawned) => {\n            eprintln!(\"DAEMON_STARTED\");\n            0\n        }\n        Err(e) => {\n            eprintln!(\"DAEMON_START_FAIL: {e}\");\n            1\n        }\n    }\n}\n\n/// `spt daemon stop` — graceful, service-aware shutdown (REQ-DAEMON-6, M8\n/// decision 5 / REQ-CLI-2): a managed service is stopped through its manager\n/// (so it does not auto-restart-fight); a manual daemon acks the seed-control\n/// stop then exits. Idempotent UX: not-running is already-stopped (exit 0).\n/// The live-session stop guard (REQ-DAEMON-STOP-LIVE-SESSION-WARN): pure — a daemon\n/// hosting live sessions REFUSES to stop (naming them) unless `--force`, so a stop\n/// never silently kills hosted endpoints out from under an operator. Empty set (no\n/// hosted sessions) or `--force` ⇒ proceed. `Err` carries the operator warning.\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn stop_live_session_guard(live_sessions: &[String], force: bool) -> Result<(), String> {\n    if live_sessions.is_empty() || force {\n        return Ok(());\n    }\n    Err(format!(\n        \"the daemon is hosting {} live session(s) that stopping would kill: {}. \\\n         Re-run with `spt daemon stop --force` to stop anyway — they come back on the \\\n         next `spt daemon start` — or leave the daemon running.\",\n        live_sessions.len(),\n        live_sessions.join(\", \")\n    ))\n}\n\n/// The endpoint ids the running daemon currently hosts a broker PTY session for —\n/// the stop-guard's \"would be killed\" set. Best-effort: a down/unreachable daemon\n/// yields none (the guard is then a no-op and the normal stop path reports\n/// already-stopped). // [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn live_hosted_session_ids() -> Vec<String> {\n    if !spt_daemon::is_running() {\n        return Vec::new();\n    }\n    match spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms()) {\n        Ok(mut b) => b\n            .sessions()\n            .map(|r| {\n                r.sessions\n                    .into_iter()\n                    .map(|s| s.endpoint)\n                    .filter(|e| !e.is_empty())\n                    .collect()\n            })\n            .unwrap_or_default(),\n        Err(_) => Vec::new(),\n    }\n}\n\n// [impl->REQ-DAEMON-6]\n// [impl->REQ-CLI-2]\n// [impl->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\nfn cmd_daemon_stop(force: bool) -> i32 {\n    // Guard: refuse + name the sessions if stopping would kill live hosted endpoints,\n    // unless --force. Runs BEFORE the stop so nothing is torn down on a refusal.\n    let live = live_hosted_session_ids();\n    if let Err(msg) = stop_live_session_guard(&live, force) {\n        eprintln!(\"DAEMON_STOP_REFUSED: {msg}\");\n        return 3;\n    }\n    match spt_daemon::stop_daemon() {\n        Ok(spt_daemon::StopOutcome::NotRunning) => {\n            eprintln!(\"DAEMON_NOT_RUNNING (already stopped)\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::ServiceStopped(label)) => {\n            eprintln!(\"DAEMON_STOPPED (via {label})\");\n            0\n        }\n        Ok(spt_daemon::StopOutcome::IpcStopped) => {\n            eprintln!(\"DAEMON_STOPPED\");\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)\" +","startLine":536,"lineNumbers":[536,null,539,null,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,null,610,null,1262,null,1289,null,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,null,3940,3941,3942,3943,3944,3945,3946,3947,3948,3949,3950,3951,3952,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3973,3974,3975,3976,3977,3978,3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3992,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"}}},"isError":false,"timestamp":1784195829777}}
{"type":"message","id":"5f3ce8cf","parentId":"b3ce29b1","timestamp":"2026-07-16T09:57:09.814Z","message":{"role":"toolResult","toolCallId":"call_eu665FXO6rUEyecAkJyvl4vn|fc_0698befac979932d016a58aaf53168819ab5ec079740e4fb7f","toolName":"read","content":[{"type":"text","text":"    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n…\n        if !self.viewers.is_empty() {\n…\n            if !evicted.is_empty() {\n                for vid in evicted {\n…\n                }\n                self.stamp_viewer_count();\n            }\n        }\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n        // slow controller falls behind the ring) unless it has been Full for the\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n        let now = Instant::now();\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n                Ok(()) => {\n                    c.last_ok = now;\n                    None\n                }\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n                        Some(epoch) // stalled past the deadline → bounded detach\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n…\n\n    /// The live endpoints running `adapter`, each with its install dir — the apply\n    /// targets for a live adapter update (W3d decides daemon-apply vs CLI-direct\n    /// from whether this is empty). Distinct, sorted. (W3a, ADR-0025)\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\n    fn endpoints_running_adapter(&self, adapter: &str) -> Vec<(String, std::path::PathBuf)> {\n        let rows: Vec<_> = {\n            let map = recover(&self.sessions);\n            map.values()\n                .map(|s| (s.endpoint.clone(), s.adapter.clone(), s.install_dir.clone()))\n                .collect()\n        };\n        select_endpoints_running_adapter(rows, adapter)\n    }\n\n    /// The child pid of a hosted session, if present (tests assert it is stable\n    /// across brain restarts).\n    pub fn session_pid(&self, id: u64) -> Option<u32> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .and_then(|h| h.session.process_id())\n    }\n\n    /// Broker-truth read for the brain promotion DRAINED gate\n    /// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the\n    /// brain's own `by: None` conn) still WEDGED — blocked in a socket write past the\n    /// brain-write deadline? The brain supervisor (in THIS broker process — no round-\n    /// trip) promotes a ready update candidate only once this reads `false`: an OLD\n    /// generation whose local controller conn is black-holed reads wedged until the W2\n    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\n    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\n    /// closed old conn is simply absent → `false` at once (the normal fast promote).\n    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\n    /// only `write_blocked_since` under each log lock, never any I/O.\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    pub fn any_local_controller_wedged(&self) -> bool {\n        let now = Instant::now();\n        let deadline = brain_write_deadline();\n        let sessions = match self.sessions.lock() {\n            Ok(g) => g,\n            Err(p) => p.into_inner(),\n        };\n        sessions.values().any(|h| {\n            match h.log.lock() {\n                Ok(log) => log.local_controller_wedged(now, deadline),\n                // A poisoned log lock cannot be honestly read — treat as NOT wedged\n                // (fail toward promotion, never a permanent gate that strands apply).\n                Err(_) => false,\n            }\n        })\n    }\n\n    /// The count of live QUIC connections the broker-owned net endpoint holds\n    /// (D4a, ADR-0004 §B), or 0 when net-less. A direct read of the broker's own\n    /// state — the survival-assertion seam for the D7-1 process-level E2E: a QUIC\n    /// connection the broker holds must outlive a brain-PROCESS swap (restoration\n    /// D7-1), the net sibling of [`session_count`](Self::session_count).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn net_conn_count(&self) -> usize {\n        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)\n    }\n\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n…\n                }\n…\n            }\n        }\n…\n    }\n…\n    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        match resolved {\n            Some(mut translation) => {\n…\n                // A native caller ignores the hint and decides spool-vs-drop by its\n                // prefer/force flag. Raw `payload + \\r` is NOT a fallback (it does not\n                // submit on a modern TUI — the F-019 silent pseudo-delivery); the\n                // inbound is never lost (spooled, poll-fed) and the miss is LOUD.\n                // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n                // [impl->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\n                if !idle {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: endpoint ACTIVE -> spool (deferred hint), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, true));\n                } else {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: no working translation binary (absent/faulted/worker-gone) -> SPOOLED (idle window), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                }\n                Ok(())\n            }\n            // No hosted session for this endpoint — tell the caller to spool\n            // NON-deferred (idle-eligible; a non-hosted target has no active window).\n            None => {\n                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                Ok(())\n            }\n        }\n    }\n\n    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker\n    /// rejects a resize arriving on any stream that is not the session's current\n    /// controller (a viewer's resize is refused), then updates the stored size\n    /// and pushes a `size` frame to every viewer (the letterbox feed).\n    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: ResizeReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad resize payload: {e}\"))?;\n        let (session, 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.session), Arc::clone(&h.log))\n        };\n        if !recover_log(&log).is_controller(send) {\n            return Err(\n                \"resize is controller-exclusive: only the interactive controller may resize\"\n                    .to_string(),\n            );\n        }\n        session\n            .resize(SurfaceSize::new(req.rows, req.cols))\n            .map_err(|e| format!(\"resize failed: {e}\"))?;\n        recover_log(&log).set_size_and_notify(req.rows, req.cols);\n        Ok(())\n    }\n\n    /// Report the broker-owned net endpoint's status (D4a). Answered even when\n    /// the broker has no network host (`enabled: false`) so a brain can probe\n    /// capability without treating absence as an error.\n    // [impl->REQ-DAEMON-2]\n    fn dispatch_net_status(&self, send: &SharedSend) {\n        let reply = match self.net.get() {\n            Some(host) => NetStatusReply {\n                enabled: true,\n                node_id_hex: Some(host.node_id_hex()),\n                addr: host.addr_json(),\n                conn_count: host.conn_count(),\n            },\n            None => NetStatusReply {\n                enabled: false,\n                node_id_hex: None,\n                addr: serde_json::Value::Null,\n                conn_count: 0,\n            },\n        };\n        let frame = Envelope::new(\n            KIND_NET_STATUS_REPLY,\n            serde_json::to_value(reply).expect(\"NetStatusReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Report the broker's OWN compiled image version (F-025). `env!` bakes the\n    /// version at build time, so a broker that SURVIVED a `spt update apply`\n    /// (brain-only restart, ADR-0018 D3-3) honestly reports the PRE-apply version\n    /// — the authoritative running-image truth the `daemon status` version surface\n    /// compares against the freshly-installed CLI. Sourced from the running\n    /// process, never inferred from disk (the disk is the half already ahead).\n    // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n    fn dispatch_broker_image(&self, send: &SharedSend) {\n        let frame = Envelope::new(\n            KIND_BROKER_IMAGE_REPLY,\n            serde_json::to_value(BrokerImageReply {\n                // spt-daemon's CARGO_PKG_VERSION == the workspace version this\n                // broker binary was compiled at.\n                version: env!(\"CARGO_PKG_VERSION\").to_string(),\n            })\n            .expect(\"BrokerImageReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Answer the brain-subscriber stall-evict tally query (LIFECYCLE-TRUTH W2\n    /// observability rider) from the broker-global counters — so `daemon status` can\n    /// surface a brain that never returned as a diagnosable fact, not a silent absence.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn dispatch_stall_evicts(&self, send: &SharedSend) {\n        let (count, last_ms) = stall_evict_stats();\n        let frame = Envelope::new(\n            KIND_STALL_EVICTS_REPLY,\n            serde_json::to_value(StallEvictsReply { count, last_ms })\n                .expect(\"StallEvictsReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Handle a planned brain-restart request (ADR-0018 D3-3, Q3): raise the\n    /// supervisor's restart signal so it cycles the brain child onto the swapped\n    /// binary with `start-reason=update`, then ack. `honored` reflects whether a\n    /// supervisor is wired (a bare test broker has none → accepted-but-unhonored).\n    /// The broker — which observes every respawn — stamps the `update` reason,\n    /// keeping planned-vs-crash authoritative on this side (V2).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-UPD-3]\n    fn dispatch_brain_restart(&self, send: &SharedSend) {\n        let honored = match self.brain_restart.get() {\n            Some(signal) => {\n                signal.request();\n                true\n            }\n            None => false,\n        };\n        let frame = Envelope::new(\n            KIND_BRAIN_RESTARTED,\n            serde_json::to_value(BrainRestarted { honored })\n                .expect(\"BrainRestarted serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a).\n    ///\n    /// With an `op_id` the dial is routed through the broker-owned\n    /// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,\n    /// exactly like an idempotent PTY write: a replay of the same id after a\n    /// brain crash is deduped (no second connection) and answered with the\n    /// connection the first apply opened. The `conn_id` is recorded *inside*\n    /// the journaled closure, so a concurrent deduped replay always finds it.\n    /// A deduped id whose connection this process no longer holds (a broker\n    /// restart — the journal survives, connections don't) is a typed error\n    /// telling the brain to dial under a fresh id.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_dial(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetDialReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-dial payload: {e}\"))?;\n…\n    }"}],"details":{"displayContent":{"text":"    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n…\n        if !self.viewers.is_empty() {\n…\n            if !evicted.is_empty() {\n                for vid in evicted {\n…\n                }\n                self.stamp_viewer_count();\n            }\n        }\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n        // slow controller falls behind the ring) unless it has been Full for the\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n        let now = Instant::now();\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n                Ok(()) => {\n                    c.last_ok = now;\n                    None\n                }\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n                        Some(epoch) // stalled past the deadline → bounded detach\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n…\n\n    /// The live endpoints running `adapter`, each with its install dir — the apply\n    /// targets for a live adapter update (W3d decides daemon-apply vs CLI-direct\n    /// from whether this is empty). Distinct, sorted. (W3a, ADR-0025)\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\n    fn endpoints_running_adapter(&self, adapter: &str) -> Vec<(String, std::path::PathBuf)> {\n        let rows: Vec<_> = {\n            let map = recover(&self.sessions);\n            map.values()\n                .map(|s| (s.endpoint.clone(), s.adapter.clone(), s.install_dir.clone()))\n                .collect()\n        };\n        select_endpoints_running_adapter(rows, adapter)\n    }\n\n    /// The child pid of a hosted session, if present (tests assert it is stable\n    /// across brain restarts).\n    pub fn session_pid(&self, id: u64) -> Option<u32> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .and_then(|h| h.session.process_id())\n    }\n\n    /// Broker-truth read for the brain promotion DRAINED gate\n    /// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the\n    /// brain's own `by: None` conn) still WEDGED — blocked in a socket write past the\n    /// brain-write deadline? The brain supervisor (in THIS broker process — no round-\n    /// trip) promotes a ready update candidate only once this reads `false`: an OLD\n    /// generation whose local controller conn is black-holed reads wedged until the W2\n    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\n    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\n    /// closed old conn is simply absent → `false` at once (the normal fast promote).\n    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\n    /// only `write_blocked_since` under each log lock, never any I/O.\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    pub fn any_local_controller_wedged(&self) -> bool {\n        let now = Instant::now();\n        let deadline = brain_write_deadline();\n        let sessions = match self.sessions.lock() {\n            Ok(g) => g,\n            Err(p) => p.into_inner(),\n        };\n        sessions.values().any(|h| {\n            match h.log.lock() {\n                Ok(log) => log.local_controller_wedged(now, deadline),\n                // A poisoned log lock cannot be honestly read — treat as NOT wedged\n                // (fail toward promotion, never a permanent gate that strands apply).\n                Err(_) => false,\n            }\n        })\n    }\n\n    /// The count of live QUIC connections the broker-owned net endpoint holds\n    /// (D4a, ADR-0004 §B), or 0 when net-less. A direct read of the broker's own\n    /// state — the survival-assertion seam for the D7-1 process-level E2E: a QUIC\n    /// connection the broker holds must outlive a brain-PROCESS swap (restoration\n    /// D7-1), the net sibling of [`session_count`](Self::session_count).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn net_conn_count(&self) -> usize {\n        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)\n    }\n\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n…\n                }\n…\n            }\n        }\n…\n    }\n…\n    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        match resolved {\n            Some(mut translation) => {\n…\n                // A native caller ignores the hint and decides spool-vs-drop by its\n                // prefer/force flag. Raw `payload + \\r` is NOT a fallback (it does not\n                // submit on a modern TUI — the F-019 silent pseudo-delivery); the\n                // inbound is never lost (spooled, poll-fed) and the miss is LOUD.\n                // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n                // [impl->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\n                if !idle {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: endpoint ACTIVE -> spool (deferred hint), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, true));\n                } else {\n                    eprintln!(\n                        \"ENDPOINT_INJECT:{}: no working translation binary (absent/faulted/worker-gone) -> SPOOLED (idle window), not injected\",\n                        req.endpoint\n                    );\n                    send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                }\n                Ok(())\n            }\n            // No hosted session for this endpoint — tell the caller to spool\n            // NON-deferred (idle-eligible; a non-hosted target has no active window).\n            None => {\n                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false, false));\n                Ok(())\n            }\n        }\n    }\n\n    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker\n    /// rejects a resize arriving on any stream that is not the session's current\n    /// controller (a viewer's resize is refused), then updates the stored size\n    /// and pushes a `size` frame to every viewer (the letterbox feed).\n    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: ResizeReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad resize payload: {e}\"))?;\n        let (session, 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.session), Arc::clone(&h.log))\n        };\n        if !recover_log(&log).is_controller(send) {\n            return Err(\n                \"resize is controller-exclusive: only the interactive controller may resize\"\n                    .to_string(),\n            );\n        }\n        session\n            .resize(SurfaceSize::new(req.rows, req.cols))\n            .map_err(|e| format!(\"resize failed: {e}\"))?;\n        recover_log(&log).set_size_and_notify(req.rows, req.cols);\n        Ok(())\n    }\n\n    /// Report the broker-owned net endpoint's status (D4a). Answered even when\n    /// the broker has no network host (`enabled: false`) so a brain can probe\n    /// capability without treating absence as an error.\n    // [impl->REQ-DAEMON-2]\n    fn dispatch_net_status(&self, send: &SharedSend) {\n        let reply = match self.net.get() {\n            Some(host) => NetStatusReply {\n                enabled: true,\n                node_id_hex: Some(host.node_id_hex()),\n                addr: host.addr_json(),\n                conn_count: host.conn_count(),\n            },\n            None => NetStatusReply {\n                enabled: false,\n                node_id_hex: None,\n                addr: serde_json::Value::Null,\n                conn_count: 0,\n            },\n        };\n        let frame = Envelope::new(\n            KIND_NET_STATUS_REPLY,\n            serde_json::to_value(reply).expect(\"NetStatusReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Report the broker's OWN compiled image version (F-025). `env!` bakes the\n    /// version at build time, so a broker that SURVIVED a `spt update apply`\n    /// (brain-only restart, ADR-0018 D3-3) honestly reports the PRE-apply version\n    /// — the authoritative running-image truth the `daemon status` version surface\n    /// compares against the freshly-installed CLI. Sourced from the running\n    /// process, never inferred from disk (the disk is the half already ahead).\n    // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n    fn dispatch_broker_image(&self, send: &SharedSend) {\n        let frame = Envelope::new(\n            KIND_BROKER_IMAGE_REPLY,\n            serde_json::to_value(BrokerImageReply {\n                // spt-daemon's CARGO_PKG_VERSION == the workspace version this\n                // broker binary was compiled at.\n                version: env!(\"CARGO_PKG_VERSION\").to_string(),\n            })\n            .expect(\"BrokerImageReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Answer the brain-subscriber stall-evict tally query (LIFECYCLE-TRUTH W2\n    /// observability rider) from the broker-global counters — so `daemon status` can\n    /// surface a brain that never returned as a diagnosable fact, not a silent absence.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn dispatch_stall_evicts(&self, send: &SharedSend) {\n        let (count, last_ms) = stall_evict_stats();\n        let frame = Envelope::new(\n            KIND_STALL_EVICTS_REPLY,\n            serde_json::to_value(StallEvictsReply { count, last_ms })\n                .expect(\"StallEvictsReply serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Handle a planned brain-restart request (ADR-0018 D3-3, Q3): raise the\n    /// supervisor's restart signal so it cycles the brain child onto the swapped\n    /// binary with `start-reason=update`, then ack. `honored` reflects whether a\n    /// supervisor is wired (a bare test broker has none → accepted-but-unhonored).\n    /// The broker — which observes every respawn — stamps the `update` reason,\n    /// keeping planned-vs-crash authoritative on this side (V2).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-UPD-3]\n    fn dispatch_brain_restart(&self, send: &SharedSend) {\n        let honored = match self.brain_restart.get() {\n            Some(signal) => {\n                signal.request();\n                true\n            }\n            None => false,\n        };\n        let frame = Envelope::new(\n            KIND_BRAIN_RESTARTED,\n            serde_json::to_value(BrainRestarted { honored })\n                .expect(\"BrainRestarted serializes\"),\n        );\n        send_frame(send, &frame);\n    }\n\n    /// Dial a peer over the broker-owned endpoint (D4a).\n    ///\n    /// With an `op_id` the dial is routed through the broker-owned\n    /// [`EffectJournal`] under the reserved [`NET_EFFECT_SESSION`] namespace,\n    /// exactly like an idempotent PTY write: a replay of the same id after a\n    /// brain crash is deduped (no second connection) and answered with the\n    /// connection the first apply opened. The `conn_id` is recorded *inside*\n    /// the journaled closure, so a concurrent deduped replay always finds it.\n    /// A deduped id whose connection this process no longer holds (a broker\n    /// restart — the journal survives, connections don't) is a typed error\n    /// telling the brain to dial under a fresh id.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_dial(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetDialReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-dial payload: {e}\"))?;\n…\n    }","startLine":795,"lineNumbers":[795,null,804,null,811,812,null,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,null,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,null,3029,null,3194,3195,null,3216,null,3685,null,3702,3703,null,3760,3761,3762,3763,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,3822,3823,3824,3825,3826,3827,3828,3829,3830,3831,3832,3833,3834,3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851,3852,3853,3854,3855,3856,3857,3858,3859,3860,3861,3862,3863,3864,3865,3866,3867,3868,3869,3870,3871,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885,3886,3887,3888,3889,3890,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3903,3904,3905,3906,3907,3908,3909,3910,3911,3912,3913,3914,3915,null,3965]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784195829814}}
{"type":"message","id":"62dfde98","parentId":"5f3ce8cf","timestamp":"2026-07-16T09:57:09.822Z","message":{"role":"toolResult","toolCallId":"call_v2ZuKFTNvhGoOPQxjXKlt9np|fc_0698befac979932d016a58aaf5316c819a9b67e157ccfe414c","toolName":"read","content":[{"type":"text","text":"        {\n…\n            for name in &att.detached {\n…\n                // [impl->REQ-CLI-OUTPUT-MARKDOWN]\n                eprintln!(\"SUBNET_DETACHED:{name} (startup default — run: spt subnet attach {name})\");\n            }\n        }\n\n        // The broker's network host (D4a, ADR-0004 §B): the node's iroh endpoint\n        // + live QUIC connections live in the broker so they survive a brain\n        // restart. Identity is the durable node.key (stable name across daemon\n        // restarts); relay stance comes from daemon.json; mDNS rides the\n        // endpoint (closing the §B mDNS/relay-session row by construction). A\n        // net failure degrades to a net-less broker — it must not cost PTY\n        // hosting (mirrors the broker-bind soft-fail below).\n        // [impl->REQ-DAEMON-2]\n        // Identity (the durable node.key) decides node_hex independently of the\n        // net bring-up: a NET_BIND_FAIL (e.g. the autostart beat the network up\n        // at boot — REQ-DAEMON-9) is transient and retried, but a broken\n        // identity is not, so we only arm the retry when identity is sound.\n        let node_hex: Option<String> = spt_store::nodeid::load_or_create()\n            .ok()\n            .map(|i| i.public_key().to_hex());\n        let net = if node_hex.is_some() {\n            try_start_net()\n        } else {\n            eprintln!(\"NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry\");\n            None\n        };\n\n        // The broker hosts PTYs (B1/B2) + the net endpoint (D4a). Serve it on a\n        // background thread; a bind failure there must not stop the seed channel\n        // (the B6 surface) — the PTY-hosting consumers arrive in B7/B8.\n        let net_up = net.is_some();\n        // Keep the broker handle alive past the bind so the brain supervisor's\n        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n        let broker_handle: Option<Arc<Broker>> =\n            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n                Ok(broker) => {\n                    let serve_broker = Arc::clone(&broker);\n                    thread::spawn(move || {\n                        let _ = serve_broker.serve();\n                    });\n                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n                // so they live with the restartable brain and respawn with it.\n                // The broker keeps only the NetHost bring-up and the boot-race\n                // self-heal that binds it; the brain polls `net-status` and\n                // starts the consumers once net reports enabled.\n                // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n                    if !net_up && node_hex.is_some() {\n                        // Boot-race self-heal (REQ-DAEMON-9): net failed to bind\n                        // but identity is sound — almost always the autostart\n                        // racing the network up at boot. Retry net in the\n                        // background and attach it to the broker when it\n                        // succeeds, so the node becomes reachable WITHOUT a\n                        // manual restart; the brain notices `net-status` flip\n                        // enabled and starts the consumers then.\n                        // [impl->REQ-DAEMON-9]\n                        let broker = Arc::clone(&broker);\n                        thread::spawn(move || net_retry_attach(broker));\n                    }\n                    // Endpoint autostart replay (F-038 W5, REQ-ENDPOINT-AUTOSTART):\n                    // bring saved startup defaults back up against the broker we\n                    // just bound. Background thread — a saved endpoint that fails\n                    // to come up logs loud and never blocks daemon start, the\n                    // seed channel, or the other replays.\n                    // [impl->REQ-ENDPOINT-AUTOSTART]\n                    thread::spawn(|| {\n                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n                    });\n                    // The node-local docs server (ADR-0036 §4): broker-side so\n                    // docs are up whenever the daemon is. Loopback only; a bind\n                    // failure (port squatted) logs loud and the daemon runs on —\n                    // docs are a degradable surface, never a boot blocker.\n                    // [impl->REQ-DOCS-LOCAL-SERVER]\n                    {\n                        let port = crate::docshost::resolve_docs_port(\n                            crate::config::DaemonConfig::load().docs_port,\n                            std::env::var(crate::docshost::DOCS_PORT_ENV).ok().as_deref(),\n                        );\n                        let docs_root = spt_store::perch::spt_home().join(\"docs\");\n                        match crate::docshost::start(docs_root, port) {\n                            Ok(bound) => eprintln!(\n                                \"DOCS_SERVER_UP: {}\",\n                                crate::docshost::docs_url(bound)\n                            ),\n                            Err(e) => eprintln!(\n                                \"DOCS_SERVER_BIND_FAIL: port {port}: {e} — docs surface \\\n                                 unavailable this run (daemon continues)\"\n                            ),\n                        }\n                    }\n                    Some(broker)\n                }\n                Err(e) => {\n                    eprintln!(\"BROKER_BIND_FAIL: {e}\");\n                    None\n                }\n            };\n\n        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n…\nfn net_retry_attach(broker: Arc<Broker>) {\n…\n    loop {\n…\n        backoff = net_retry_backoff(backoff);\n        eprintln!(\"NET_BIND_RETRY: net still unavailable, retrying in {}s\", backoff.as_secs());\n    }\n}\n\nconst NET_RETRY_FIRST: Duration = Duration::from_secs(1);\nconst NET_RETRY_CAP: Duration = Duration::from_secs(30);\n\n/// The net self-heal backoff ladder (REQ-DAEMON-9): double each retry, capped\n/// at [`NET_RETRY_CAP`]. Pure for tests.\nfn net_retry_backoff(cur: Duration) -> Duration {\n    (cur * 2).min(NET_RETRY_CAP)\n}\n\n/// Whether a daemon is already serving the seed-control channel for this home.\npub fn is_running() -> bool {\n    seedmap::ping(&seed_socket_name()).is_ok()\n}\n\n/// Gracefully stop the daemon serving this home (`spt daemon stop`, M8\n/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the\n/// daemon's foreground loop returns and the process exits cleanly.\n// [impl->REQ-CLI-2]\npub fn request_stop() -> io::Result<()> {\n    seedmap::request_stop(&seed_socket_name())\n}\n\n/// Block (bounded) until a daemon answers the seed-control channel, or time\n/// out. Shared by every start path (`ensure_running`, `start_daemon`):\n/// race-tolerant — if two callers spawn at once, one wins the bind and this\n/// passes either way.\nfn wait_until_up() -> io::Result<()> {\n    let mut waited = Duration::ZERO;\n    while waited < START_TIMEOUT {\n        if is_running() {\n            return Ok(());\n        }\n        thread::sleep(START_POLL);\n        waited += START_POLL;\n    }\n    Err(io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"daemon did not come up within the start window\",\n    ))\n}\n\n/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if\n/// one already answers; otherwise bring it up and wait (bounded) for the bind.\n///\n/// Service-aware (REQ-DAEMON-8): when an OS service manager has a registered\n/// spt-daemon, this starts THAT — never a competing manual `spawn_detached`\n/// daemon that would fight the service for the broker socket (the kitsubito\n/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n// [impl->REQ-DAEMON-8]\npub fn ensure_running() -> io::Result<()> {\n    if is_running() {\n        return Ok(());\n    }\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(false, svc.detected()) {\n        crate::service::StartPlan::ServiceStart => {\n            // Prefer the manager — but if it can't be driven (no session bus on\n            // a default-home box, manager not running), fall back to a manual\n            // daemon rather than leave the caller daemon-less. The autostart\n            // path must never hard-fail when a spawn would have worked.\n            if let Err(e) = svc.start() {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n            }\n        }\n        crate::service::StartPlan::ManualSpawn => spawn_detached()?,\n        // `running` was passed false, so AlreadyRunning is unreachable here.\n        crate::service::StartPlan::AlreadyRunning => return Ok(()),\n    }\n    wait_until_up()\n}\n\n/// Outcome of a service-aware `daemon start` (REQ-DAEMON-6), so the CLI can\n/// report truthfully what happened.\n#[derive(Debug)]\npub enum StartOutcome {\n    /// A daemon already answered — nothing started.\n    AlreadyRunning,\n    /// Started through the named service manager.\n    ServiceStarted(&'static str),\n    /// Spawned a manual detached daemon (no manager registered).\n    Spawned,\n}\n\n/// Outcome of a service-aware `daemon stop` (REQ-DAEMON-6).\n#[derive(Debug)]\npub enum StopOutcome {\n    /// Nothing was running — already stopped.\n    NotRunning,\n    /// Stopped through the named service manager.\n    ServiceStopped(&'static str),\n    /// Stopped a manual daemon over the seed-control IPC.\n    IpcStopped,\n}\n\n/// Service-aware `spt daemon start` (REQ-DAEMON-6): ensure the daemon is up in\n/// the background, preferring the registered manager; idempotent + non-blocking\n/// past the bind wait.\n// [impl->REQ-DAEMON-6]\npub fn start_daemon() -> io::Result<StartOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(is_running(), svc.detected()) {\n        crate::service::StartPlan::AlreadyRunning => Ok(StartOutcome::AlreadyRunning),\n        crate::service::StartPlan::ServiceStart => match svc.start() {\n            Ok(()) => {\n                wait_until_up()?;\n                Ok(StartOutcome::ServiceStarted(svc.label()))\n            }\n            // Manager present but undrivable (no session bus) — fall back to a\n            // manual daemon so `daemon start` still brings one up.\n            Err(e) => {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n                wait_until_up()?;\n                Ok(StartOutcome::Spawned)\n            }\n        },\n        crate::service::StartPlan::ManualSpawn => {\n            spawn_detached()?;\n            wait_until_up()?;\n            Ok(StartOutcome::Spawned)\n        }\n    }\n}\n\n/// Service-aware `spt daemon stop` (REQ-DAEMON-6): a registered + active\n/// service is stopped through its manager (so it does not auto-restart-fight);\n/// a manual daemon takes the graceful IPC stop; nothing running is already\n/// stopped. Idempotent.\n// [impl->REQ-DAEMON-6]\npub fn stop_daemon() -> io::Result<StopOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_stop(is_running(), svc.detected(), svc.is_active()) {\n        crate::service::StopPlan::NotRunning => Ok(StopOutcome::NotRunning),\n        crate::service::StopPlan::ServiceStop => {\n            svc.stop()?;\n            Ok(StopOutcome::ServiceStopped(svc.label()))\n        }\n        crate::service::StopPlan::IpcStop => {\n            request_stop()?;\n            Ok(StopOutcome::IpcStopped)\n        }\n    }\n}\n\n/// The label of a managed service that is BOTH registered and active — a\n/// conflict for an inline `daemon run` that is about to take the same socket\n/// (REQ-DAEMON-7). `None` when no manager would fight.\n// [impl->REQ-DAEMON-7]\npub fn active_service_conflict() -> Option<&'static str> {\n    let svc = crate::service::platform_service();\n    (svc.detected() && svc.is_active()).then(|| svc.label())\n}\n\n/// Spawn `spt daemon` as a detached background process. Inherits the environment\n/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical"}],"details":{"displayContent":{"text":"        {\n…\n            for name in &att.detached {\n…\n                // [impl->REQ-CLI-OUTPUT-MARKDOWN]\n                eprintln!(\"SUBNET_DETACHED:{name} (startup default — run: spt subnet attach {name})\");\n            }\n        }\n\n        // The broker's network host (D4a, ADR-0004 §B): the node's iroh endpoint\n        // + live QUIC connections live in the broker so they survive a brain\n        // restart. Identity is the durable node.key (stable name across daemon\n        // restarts); relay stance comes from daemon.json; mDNS rides the\n        // endpoint (closing the §B mDNS/relay-session row by construction). A\n        // net failure degrades to a net-less broker — it must not cost PTY\n        // hosting (mirrors the broker-bind soft-fail below).\n        // [impl->REQ-DAEMON-2]\n        // Identity (the durable node.key) decides node_hex independently of the\n        // net bring-up: a NET_BIND_FAIL (e.g. the autostart beat the network up\n        // at boot — REQ-DAEMON-9) is transient and retried, but a broken\n        // identity is not, so we only arm the retry when identity is sound.\n        let node_hex: Option<String> = spt_store::nodeid::load_or_create()\n            .ok()\n            .map(|i| i.public_key().to_hex());\n        let net = if node_hex.is_some() {\n            try_start_net()\n        } else {\n            eprintln!(\"NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry\");\n            None\n        };\n\n        // The broker hosts PTYs (B1/B2) + the net endpoint (D4a). Serve it on a\n        // background thread; a bind failure there must not stop the seed channel\n        // (the B6 surface) — the PTY-hosting consumers arrive in B7/B8.\n        let net_up = net.is_some();\n        // Keep the broker handle alive past the bind so the brain supervisor's\n        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n        let broker_handle: Option<Arc<Broker>> =\n            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n                Ok(broker) => {\n                    let serve_broker = Arc::clone(&broker);\n                    thread::spawn(move || {\n                        let _ = serve_broker.serve();\n                    });\n                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n                // so they live with the restartable brain and respawn with it.\n                // The broker keeps only the NetHost bring-up and the boot-race\n                // self-heal that binds it; the brain polls `net-status` and\n                // starts the consumers once net reports enabled.\n                // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n                    if !net_up && node_hex.is_some() {\n                        // Boot-race self-heal (REQ-DAEMON-9): net failed to bind\n                        // but identity is sound — almost always the autostart\n                        // racing the network up at boot. Retry net in the\n                        // background and attach it to the broker when it\n                        // succeeds, so the node becomes reachable WITHOUT a\n                        // manual restart; the brain notices `net-status` flip\n                        // enabled and starts the consumers then.\n                        // [impl->REQ-DAEMON-9]\n                        let broker = Arc::clone(&broker);\n                        thread::spawn(move || net_retry_attach(broker));\n                    }\n                    // Endpoint autostart replay (F-038 W5, REQ-ENDPOINT-AUTOSTART):\n                    // bring saved startup defaults back up against the broker we\n                    // just bound. Background thread — a saved endpoint that fails\n                    // to come up logs loud and never blocks daemon start, the\n                    // seed channel, or the other replays.\n                    // [impl->REQ-ENDPOINT-AUTOSTART]\n                    thread::spawn(|| {\n                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n                    });\n                    // The node-local docs server (ADR-0036 §4): broker-side so\n                    // docs are up whenever the daemon is. Loopback only; a bind\n                    // failure (port squatted) logs loud and the daemon runs on —\n                    // docs are a degradable surface, never a boot blocker.\n                    // [impl->REQ-DOCS-LOCAL-SERVER]\n                    {\n                        let port = crate::docshost::resolve_docs_port(\n                            crate::config::DaemonConfig::load().docs_port,\n                            std::env::var(crate::docshost::DOCS_PORT_ENV).ok().as_deref(),\n                        );\n                        let docs_root = spt_store::perch::spt_home().join(\"docs\");\n                        match crate::docshost::start(docs_root, port) {\n                            Ok(bound) => eprintln!(\n                                \"DOCS_SERVER_UP: {}\",\n                                crate::docshost::docs_url(bound)\n                            ),\n                            Err(e) => eprintln!(\n                                \"DOCS_SERVER_BIND_FAIL: port {port}: {e} — docs surface \\\n                                 unavailable this run (daemon continues)\"\n                            ),\n                        }\n                    }\n                    Some(broker)\n                }\n                Err(e) => {\n                    eprintln!(\"BROKER_BIND_FAIL: {e}\");\n                    None\n                }\n            };\n\n        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n…\nfn net_retry_attach(broker: Arc<Broker>) {\n…\n    loop {\n…\n        backoff = net_retry_backoff(backoff);\n        eprintln!(\"NET_BIND_RETRY: net still unavailable, retrying in {}s\", backoff.as_secs());\n    }\n}\n\nconst NET_RETRY_FIRST: Duration = Duration::from_secs(1);\nconst NET_RETRY_CAP: Duration = Duration::from_secs(30);\n\n/// The net self-heal backoff ladder (REQ-DAEMON-9): double each retry, capped\n/// at [`NET_RETRY_CAP`]. Pure for tests.\nfn net_retry_backoff(cur: Duration) -> Duration {\n    (cur * 2).min(NET_RETRY_CAP)\n}\n\n/// Whether a daemon is already serving the seed-control channel for this home.\npub fn is_running() -> bool {\n    seedmap::ping(&seed_socket_name()).is_ok()\n}\n\n/// Gracefully stop the daemon serving this home (`spt daemon stop`, M8\n/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the\n/// daemon's foreground loop returns and the process exits cleanly.\n// [impl->REQ-CLI-2]\npub fn request_stop() -> io::Result<()> {\n    seedmap::request_stop(&seed_socket_name())\n}\n\n/// Block (bounded) until a daemon answers the seed-control channel, or time\n/// out. Shared by every start path (`ensure_running`, `start_daemon`):\n/// race-tolerant — if two callers spawn at once, one wins the bind and this\n/// passes either way.\nfn wait_until_up() -> io::Result<()> {\n    let mut waited = Duration::ZERO;\n    while waited < START_TIMEOUT {\n        if is_running() {\n            return Ok(());\n        }\n        thread::sleep(START_POLL);\n        waited += START_POLL;\n    }\n    Err(io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"daemon did not come up within the start window\",\n    ))\n}\n\n/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if\n/// one already answers; otherwise bring it up and wait (bounded) for the bind.\n///\n/// Service-aware (REQ-DAEMON-8): when an OS service manager has a registered\n/// spt-daemon, this starts THAT — never a competing manual `spawn_detached`\n/// daemon that would fight the service for the broker socket (the kitsubito\n/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n// [impl->REQ-DAEMON-8]\npub fn ensure_running() -> io::Result<()> {\n    if is_running() {\n        return Ok(());\n    }\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(false, svc.detected()) {\n        crate::service::StartPlan::ServiceStart => {\n            // Prefer the manager — but if it can't be driven (no session bus on\n            // a default-home box, manager not running), fall back to a manual\n            // daemon rather than leave the caller daemon-less. The autostart\n            // path must never hard-fail when a spawn would have worked.\n            if let Err(e) = svc.start() {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n            }\n        }\n        crate::service::StartPlan::ManualSpawn => spawn_detached()?,\n        // `running` was passed false, so AlreadyRunning is unreachable here.\n        crate::service::StartPlan::AlreadyRunning => return Ok(()),\n    }\n    wait_until_up()\n}\n\n/// Outcome of a service-aware `daemon start` (REQ-DAEMON-6), so the CLI can\n/// report truthfully what happened.\n#[derive(Debug)]\npub enum StartOutcome {\n    /// A daemon already answered — nothing started.\n    AlreadyRunning,\n    /// Started through the named service manager.\n    ServiceStarted(&'static str),\n    /// Spawned a manual detached daemon (no manager registered).\n    Spawned,\n}\n\n/// Outcome of a service-aware `daemon stop` (REQ-DAEMON-6).\n#[derive(Debug)]\npub enum StopOutcome {\n    /// Nothing was running — already stopped.\n    NotRunning,\n    /// Stopped through the named service manager.\n    ServiceStopped(&'static str),\n    /// Stopped a manual daemon over the seed-control IPC.\n    IpcStopped,\n}\n\n/// Service-aware `spt daemon start` (REQ-DAEMON-6): ensure the daemon is up in\n/// the background, preferring the registered manager; idempotent + non-blocking\n/// past the bind wait.\n// [impl->REQ-DAEMON-6]\npub fn start_daemon() -> io::Result<StartOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_start(is_running(), svc.detected()) {\n        crate::service::StartPlan::AlreadyRunning => Ok(StartOutcome::AlreadyRunning),\n        crate::service::StartPlan::ServiceStart => match svc.start() {\n            Ok(()) => {\n                wait_until_up()?;\n                Ok(StartOutcome::ServiceStarted(svc.label()))\n            }\n            // Manager present but undrivable (no session bus) — fall back to a\n            // manual daemon so `daemon start` still brings one up.\n            Err(e) => {\n                eprintln!(\"DAEMON_SERVICE_START_FALLBACK: {e} — starting a manual daemon\");\n                spawn_detached()?;\n                wait_until_up()?;\n                Ok(StartOutcome::Spawned)\n            }\n        },\n        crate::service::StartPlan::ManualSpawn => {\n            spawn_detached()?;\n            wait_until_up()?;\n            Ok(StartOutcome::Spawned)\n        }\n    }\n}\n\n/// Service-aware `spt daemon stop` (REQ-DAEMON-6): a registered + active\n/// service is stopped through its manager (so it does not auto-restart-fight);\n/// a manual daemon takes the graceful IPC stop; nothing running is already\n/// stopped. Idempotent.\n// [impl->REQ-DAEMON-6]\npub fn stop_daemon() -> io::Result<StopOutcome> {\n    let svc = crate::service::platform_service();\n    match crate::service::plan_stop(is_running(), svc.detected(), svc.is_active()) {\n        crate::service::StopPlan::NotRunning => Ok(StopOutcome::NotRunning),\n        crate::service::StopPlan::ServiceStop => {\n            svc.stop()?;\n            Ok(StopOutcome::ServiceStopped(svc.label()))\n        }\n        crate::service::StopPlan::IpcStop => {\n            request_stop()?;\n            Ok(StopOutcome::IpcStopped)\n        }\n    }\n}\n\n/// The label of a managed service that is BOTH registered and active — a\n/// conflict for an inline `daemon run` that is about to take the same socket\n/// (REQ-DAEMON-7). `None` when no manager would fight.\n// [impl->REQ-DAEMON-7]\npub fn active_service_conflict() -> Option<&'static str> {\n    let svc = crate::service::platform_service();\n    (svc.detected() && svc.is_active()).then(|| svc.label())\n}\n\n/// Spawn `spt daemon` as a detached background process. Inherits the environment\n/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical","startLine":143,"lineNumbers":[143,null,150,null,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,null,440,null,442,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\daemon.rs"}}},"isError":false,"timestamp":1784195829822}}
{"type":"message","id":"8d848722","parentId":"62dfde98","timestamp":"2026-07-16T09:57:09.824Z","message":{"role":"toolResult","toolCallId":"call_bB22GSLbuCScc8bZdv3E2ej6|fc_0698befac979932d016a58aaf531a4819a9f8231624d5f7210","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## brainproc.rs\n 1043|    // broker has a console (a `spt daemon run` in a terminal, or any test that\n*1044|    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n 1045|    // the plain-child SPT_HOME inheritance (above) is preserved.\n 1046|    #[cfg(windows)]\n 1047|    {\n 1048|        use std::os::windows::process::CommandExt;\n*1049|        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n*1050|        cmd.creation_flags(CREATE_NO_WINDOW);\n 1051|    }\n 1052|    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n 1053|    // OWN process group so the daemon can reap the brain + everything it spawns\n...\n 1060|        unsafe {\n*1061|            cmd.pre_exec(|| {\n 1062|                // setpgid(0, 0): this child becomes the leader of a new group\n 1063|                // (pgid == its pid). Best-effort — a failure only loses the reap.\n 1064|                libc::setpgid(0, 0);\n...\n 1068|    }\n*1069|    cmd.spawn()\n 1070|}\n 1071|\n 1072|/// The versioned brain-spawn argv (KH-2.3): `daemon brain --generation <N>\n...\n 1096|    stop: Arc<AtomicBool>,\n*1097|    reaper: Arc<crate::reap::BrainReaper>,\n 1098|    broker: Option<Arc<crate::broker::Broker>>,\n 1099|) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n 1100|    let restart = Arc::new(BrainRestart::new());\n...\n 1127|                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n*1128|                reaper.enroll(&child);\n 1129|                Ok(child)\n 1130|            },\n 1131|        );\n...\n 1145|        {\n*1146|            Command::new(\"cmd\").args([\"/C\", \"rem\"]).spawn()\n 1147|        }\n 1148|        #[cfg(unix)]\n 1149|        {\n*1150|            Command::new(\"true\").spawn()\n 1151|        }\n 1152|    }\n 1153|\n...\n 1162|                .args([\"/C\", \"ping\", \"-n\", \"30\", \"127.0.0.1\"])\n*1163|                .stdout(std::process::Stdio::null())\n*1164|                .spawn()\n 1165|        }\n 1166|        #[cfg(unix)]\n 1167|        {\n*1168|            Command::new(\"sleep\").arg(\"30\").spawn()\n 1169|        }\n 1170|    }\n 1171|\n## daemon.rs\n 328|        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n*329|        let reaper = crate::reap::BrainReaper::new();\n 330|        // The supervisor stop flag — hoisted out of the wiring block (was held\n 331|        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n 332|        // it, so the supervisor does not respawn the brain we are about to reap.\n...\n 336|                Arc::clone(&brain_stop),\n*337|                Arc::clone(&reaper),\n 338|                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n 339|                // broker's live control-plane truth in-process; None on a bind-failed\n 340|                // daemon degrades the gate to readiness-only.\n...\n 366|        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);\n*367|        reaper.reap();\n 368|        serve\n 369|    }\n 370|\n...\n 651|        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n*652|        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n 653|        let args = [\n 654|            \"daemon\".to_string(),\n 655|            \"run\".to_string(),\n...\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 696|            // Drop the child to the elected default user (else the sudo\n 697|            // invoker — M8 decision 8): std clears supplementary groups and\n...\n 711|        unsafe {\n*712|            cmd.pre_exec(|| {\n 713|                if libc::setsid() == -1 {\n 714|                    return Err(std::io::Error::last_os_error());\n 715|                }\n...\n 718|        }\n*719|        cmd.spawn()?;\n 720|        Ok(())\n 721|    }\n 722|}\n...\n 744|/// capturing caller anywhere up the process chain can be wedged by it.\n*745|/// `DETACHED_PROCESS` ⇒ no console, so the child's std handles are simply absent\n 746|/// (every such child runs stdio-null by design anyway). `lpApplicationName`\n 747|/// stays NULL so `program` resolves via PATH like `Command` would. Raw\n 748|/// `CreateProcessW` declared inline to avoid a winapi dependency for one call.\n...\n 800|    }\n*801|    // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW: no\n 802|    // inherited console, no popup window, its own signal group.\n 803|    const BASE_FLAGS: u32 = 0x0000_0008 | 0x0000_0200 | 0x0800_0000;\n 804|\n...\n 873|/// The root cause this defeats: `spawn_detached` runs FROM the terminal-child\n*874|/// CLI, so a daemon it spawns directly INHERITS the terminal's Job (DETACHED_PROCESS\n 875|/// detaches the console, not the job). A job-neutral creator (WMI / Task\n 876|/// Scheduler) makes the daemon a child of WmiPrvSE / the scheduler instead —\n 877|/// outside any terminal job from birth, which is why Task-Scheduler-autostarted\n...\n 973|/// schtasks rungs launch the broker via `cmd /c … start \"\" /b spt daemon run`;\n*974|/// @15fdf58 set DETACHED_PROCESS on the cmd WRAPPER, but `start /b` then launched\n 975|/// the inner broker (a console-subsystem exe) with no console to inherit, so it\n 976|/// ALLOCATED ITS OWN conhost = a visible window (REQ-HAZARD-WMI-DAEMON-WINDOW;\n 977|/// violating REQ-INSTALL-10's no-persistent-window invariant). Calling FreeConsole\n...\n 979|/// drops that console so the conhost exits. Launch-agnostic + idempotent: a no-op\n*980|/// on the breakaway/injob rungs (already DETACHED_PROCESS ⇒ no console ⇒ FreeConsole\n 981|/// returns 0/err, ignored), and a foreground human `spt daemon run` omits the flag\n 982|/// so its terminal view is preserved.\n 983|// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]\n...\n 1001|        // three std handles so stdio degrades to a safe no-op, exactly the\n*1002|        // no-console state the DETACHED_PROCESS rungs (breakaway/injob) already run\n 1003|        // in (Rust treats a null std handle as a discard-success write).\n 1004|        let _ = SetStdHandle(STD_INPUT_HANDLE, 0);\n 1005|        let _ = SetStdHandle(STD_OUTPUT_HANDLE, 0);\n...\n 1018|/// AND every int test's `Stdio::from(file)` brain-log capture) is a disk handle that\n*1019|/// never blocks; a CONSOLE scrolls; a NULL/absent handle (the DETACHED_PROCESS rungs)\n 1020|/// already discards. So we DELIBERATELY do NOT gate on \"no console\" (`GetConsoleWindow`):\n*1021|/// a CREATE_NO_WINDOW daemon has no console window yet a perfectly-drained FILE stderr\n 1022|/// — nulling it would blank the capture (the test-harness brain-log assertions) for\n 1023|/// ZERO safety gain, since a file never blocks. `stderr_file_type` is the raw\n 1024|/// `GetFileType(stderr)`. Pure so both CI legs unit-test it. // [impl->REQ-HAZARD-DETACHED-DAEMON-STDIO]\n...\n 1099|/// Win32_Process.Create does NOT inherit the launching powershell's\n*1100|/// CREATE_NO_WINDOW — without an explicit `Win32_ProcessStartup` the spawned\n 1101|/// `cmd.exe` wrapper pops a console window (the v0.12.1 regression). The startup\n*1102|/// spec carries `CreateFlags=DETACHED_PROCESS` (0x8 — no console ⇒ no window;\n*1103|/// `CREATE_NO_WINDOW` 0x08000000 is NOT a valid Win32_ProcessStartup flag →\n 1104|/// ReturnValue 21) plus `ShowWindow=SW_HIDE`(0), keeping `spt daemon start` a\n 1105|/// console-less background daemon (v0.7.4 no-persistent-window invariant,\n 1106|/// REQ-INSTALL-10).\n## broker.rs\n 4068|    /// the CLI can collect the code, fresh, against a found member. Expired holds\n*4069|    /// from abandoned earlier attempts are swept here (no separate reaper — pairing\n 4070|    /// is rare and the map is tiny).\n 4071|    // [impl->REQ-JOIN-TWO-PHASE]\n 4072|    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {"}],"details":{"scopePath":"crates/spt-daemon/src/brainproc.rs, crates/spt-daemon/src/daemon.rs, crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/applyhost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":33,"fileCount":3,"files":["crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":12},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## brainproc.rs\n 1043│    // broker has a console (a `spt daemon run` in a terminal, or any test that\n*1044│    // spawns the daemon); CREATE_NO_WINDOW suppresses it without detaching, so\n 1045│    // the plain-child SPT_HOME inheritance (above) is preserved.\n 1046│    #[cfg(windows)]\n 1047│    {\n 1048│        use std::os::windows::process::CommandExt;\n*1049│        const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n*1050│        cmd.creation_flags(CREATE_NO_WINDOW);\n 1051│    }\n 1052│    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its\n 1053│    // OWN process group so the daemon can reap the brain + everything it spawns\n     │...\n 1060│        unsafe {\n*1061│            cmd.pre_exec(|| {\n 1062│                // setpgid(0, 0): this child becomes the leader of a new group\n 1063│                // (pgid == its pid). Best-effort — a failure only loses the reap.\n 1064│                libc::setpgid(0, 0);\n     │...\n 1068│    }\n*1069│    cmd.spawn()\n 1070│}\n 1071│\n 1072│/// The versioned brain-spawn argv (KH-2.3): `daemon brain --generation <N>\n     │...\n 1096│    stop: Arc<AtomicBool>,\n*1097│    reaper: Arc<crate::reap::BrainReaper>,\n 1098│    broker: Option<Arc<crate::broker::Broker>>,\n 1099│) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n 1100│    let restart = Arc::new(BrainRestart::new());\n     │...\n 1127│                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n*1128│                reaper.enroll(&child);\n 1129│                Ok(child)\n 1130│            },\n 1131│        );\n     │...\n 1145│        {\n*1146│            Command::new(\"cmd\").args([\"/C\", \"rem\"]).spawn()\n 1147│        }\n 1148│        #[cfg(unix)]\n 1149│        {\n*1150│            Command::new(\"true\").spawn()\n 1151│        }\n 1152│    }\n 1153│\n     │...\n 1162│                .args([\"/C\", \"ping\", \"-n\", \"30\", \"127.0.0.1\"])\n*1163│                .stdout(std::process::Stdio::null())\n*1164│                .spawn()\n 1165│        }\n 1166│        #[cfg(unix)]\n 1167│        {\n*1168│            Command::new(\"sleep\").arg(\"30\").spawn()\n 1169│        }\n 1170│    }\n 1171│\n## daemon.rs\n  328│        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n *329│        let reaper = crate::reap::BrainReaper::new();\n  330│        // The supervisor stop flag — hoisted out of the wiring block (was held\n  331│        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n  332│        // it, so the supervisor does not respawn the brain we are about to reap.\n     │...\n  336│                Arc::clone(&brain_stop),\n *337│                Arc::clone(&reaper),\n  338│                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n  339│                // broker's live control-plane truth in-process; None on a bind-failed\n  340│                // daemon degrades the gate to readiness-only.\n     │...\n  366│        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);\n *367│        reaper.reap();\n  368│        serve\n  369│    }\n  370│\n     │...\n  651│        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n *652│        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n  653│        let args = [\n  654│            \"daemon\".to_string(),\n  655│            \"run\".to_string(),\n     │...\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  696│            // Drop the child to the elected default user (else the sudo\n  697│            // invoker — M8 decision 8): std clears supplementary groups and\n     │...\n  711│        unsafe {\n *712│            cmd.pre_exec(|| {\n  713│                if libc::setsid() == -1 {\n  714│                    return Err(std::io::Error::last_os_error());\n  715│                }\n     │...\n  718│        }\n *719│        cmd.spawn()?;\n  720│        Ok(())\n  721│    }\n  722│}\n     │...\n  744│/// capturing caller anywhere up the process chain can be wedged by it.\n *745│/// `DETACHED_PROCESS` ⇒ no console, so the child's std handles are simply absent\n  746│/// (every such child runs stdio-null by design anyway). `lpApplicationName`\n  747│/// stays NULL so `program` resolves via PATH like `Command` would. Raw\n  748│/// `CreateProcessW` declared inline to avoid a winapi dependency for one call.\n     │...\n  800│    }\n *801│    // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW: no\n  802│    // inherited console, no popup window, its own signal group.\n  803│    const BASE_FLAGS: u32 = 0x0000_0008 | 0x0000_0200 | 0x0800_0000;\n  804│\n     │...\n  873│/// The root cause this defeats: `spawn_detached` runs FROM the terminal-child\n *874│/// CLI, so a daemon it spawns directly INHERITS the terminal's Job (DETACHED_PROCESS\n  875│/// detaches the console, not the job). A job-neutral creator (WMI / Task\n  876│/// Scheduler) makes the daemon a child of WmiPrvSE / the scheduler instead —\n  877│/// outside any terminal job from birth, which is why Task-Scheduler-autostarted\n     │...\n  973│/// schtasks rungs launch the broker via `cmd /c … start \"\" /b spt daemon run`;\n *974│/// @15fdf58 set DETACHED_PROCESS on the cmd WRAPPER, but `start /b` then launched\n  975│/// the inner broker (a console-subsystem exe) with no console to inherit, so it\n  976│/// ALLOCATED ITS OWN conhost = a visible window (REQ-HAZARD-WMI-DAEMON-WINDOW;\n  977│/// violating REQ-INSTALL-10's no-persistent-window invariant). Calling FreeConsole\n     │...\n  979│/// drops that console so the conhost exits. Launch-agnostic + idempotent: a no-op\n *980│/// on the breakaway/injob rungs (already DETACHED_PROCESS ⇒ no console ⇒ FreeConsole\n  981│/// returns 0/err, ignored), and a foreground human `spt daemon run` omits the flag\n  982│/// so its terminal view is preserved.\n  983│// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]\n     │...\n 1001│        // three std handles so stdio degrades to a safe no-op, exactly the\n*1002│        // no-console state the DETACHED_PROCESS rungs (breakaway/injob) already run\n 1003│        // in (Rust treats a null std handle as a discard-success write).\n 1004│        let _ = SetStdHandle(STD_INPUT_HANDLE, 0);\n 1005│        let _ = SetStdHandle(STD_OUTPUT_HANDLE, 0);\n     │...\n 1018│/// AND every int test's `Stdio::from(file)` brain-log capture) is a disk handle that\n*1019│/// never blocks; a CONSOLE scrolls; a NULL/absent handle (the DETACHED_PROCESS rungs)\n 1020│/// already discards. So we DELIBERATELY do NOT gate on \"no console\" (`GetConsoleWindow`):\n*1021│/// a CREATE_NO_WINDOW daemon has no console window yet a perfectly-drained FILE stderr\n 1022│/// — nulling it would blank the capture (the test-harness brain-log assertions) for\n 1023│/// ZERO safety gain, since a file never blocks. `stderr_file_type` is the raw\n 1024│/// `GetFileType(stderr)`. Pure so both CI legs unit-test it. // [impl->REQ-HAZARD-DETACHED-DAEMON-STDIO]\n     │...\n 1099│/// Win32_Process.Create does NOT inherit the launching powershell's\n*1100│/// CREATE_NO_WINDOW — without an explicit `Win32_ProcessStartup` the spawned\n 1101│/// `cmd.exe` wrapper pops a console window (the v0.12.1 regression). The startup\n*1102│/// spec carries `CreateFlags=DETACHED_PROCESS` (0x8 — no console ⇒ no window;\n*1103│/// `CREATE_NO_WINDOW` 0x08000000 is NOT a valid Win32_ProcessStartup flag →\n 1104│/// ReturnValue 21) plus `ShowWindow=SW_HIDE`(0), keeping `spt daemon start` a\n 1105│/// console-less background daemon (v0.7.4 no-persistent-window invariant,\n 1106│/// REQ-INSTALL-10).\n## broker.rs\n 4068│    /// the CLI can collect the code, fresh, against a found member. Expired holds\n*4069│    /// from abandoned earlier attempts are swept here (no separate reaper — pairing\n 4070│    /// is rare and the map is tiny).\n 4071│    // [impl->REQ-JOIN-TWO-PHASE]\n 4072│    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {"},"isError":false,"timestamp":1784195829824}}
{"type":"message","id":"d0cc627b","parentId":"8d848722","timestamp":"2026-07-16T09:57:09.826Z","message":{"role":"toolResult","toolCallId":"call_rMOR3nbMe5h5hZNYnqUIxu3x|fc_0698befac979932d016a58aaf53198819aa7a34d6e064b1ad7","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…\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n…\n}\n\n[Showing lines 1-288 of 298. Use :289 to continue]"}],"details":{"truncation":{"content":"//! 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","truncated":true,"truncatedBy":"lines","totalLines":298,"totalBytes":12098,"outputLines":288,"outputBytes":12098,"lastLinePartial":false,"firstLineExceedsLimit":false},"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…\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,null,290,291,null,297]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\daemon_refresh_e2e.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":298,"totalBytes":12098,"outputLines":288,"outputBytes":12098,"shownRange":{"start":1,"end":288},"nextOffset":289}}},"isError":false,"timestamp":1784195829826}}
{"type":"message","id":"a0eda30d","parentId":"d0cc627b","timestamp":"2026-07-16T09:57:09.946Z","message":{"role":"toolResult","toolCallId":"call_RjjmpclhZJYCH0B6a24EXmT7|fc_0698befac979932d016a58aaf53154819a84ce6ed41661849a","toolName":"read","content":[{"type":"text","text":"fn report_apply_outcome(outcome: spt_daemon::ApplyStagedOutcome) -> i32 {\n…\n    match outcome {\n…\n        O::AppliedDaemonless {\n            version,\n            product_version,\n        } => {\n            println!(\n                \"{}\",\n                render_applied_daemonless_message(version, &product_version)\n            );\n            0\n        }\n        O::Quarantined { version } => {\n            eprintln!(\n                \"UPDATE_APPLY_QUARANTINED:v{version} failed readiness on this node and was \\\n                 auto-rolled-back — refusing to re-apply it. Stage a newer version; the daemon \\\n                 is healthy on the previous one.\"\n            );\n            1\n        }\n        O::RefusedClass(class) => {\n            eprintln!(\"UPDATE_APPLY_REFUSED:{class:?} (full-swap orchestration is not M5 scope)\");\n            1\n        }\n        O::Rejected(reason) => {\n            eprintln!(\"UPDATE_APPLY_REJECTED:{reason}\");\n            1\n        }\n    }\n}\n\n/// `spt update apply [--finish]` — the explicit ack named by the update-consent\n/// notif. NO LONGER boots the daemon first (the REQ-UPDATE-ONE-SHOT-FINISH wart):\n/// a stopped box used to `ensure_daemon_announced` the OLD broker up just to hand\n/// off to it, guaranteeing a mixed old-broker/new-brain pair + a manual bounce.\n/// Now: daemon up ⇒ in-place brain handoff; daemon down ⇒ a daemonless swap (next\n/// start runs it); `--finish` ⇒ swap then restart the daemon onto the new bytes.\n// [impl->REQ-UPD-4]\n// [impl->REQ-UPD-3]\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\nfn cmd_update_apply(finish: bool) -> i32 {\n    let exe = match std::env::current_exe() {\n        Ok(p) => p,\n        Err(e) => {\n            eprintln!(\"CURRENT_EXE_FAIL: {e}\");\n            return 1;\n        }\n    };\n    let cache = spt_daemon::ReleaseCache::open(&perch::spt_home().join(\"releases\"));\n    let keys = perch::identity_dir().join(\"release-keys.json\");\n\n    if finish {\n        let code = cmd_update_apply_finish(&cache, &keys, &exe);\n        if code == 0 {\n            land_staged_docs(&cache);\n        }\n        return code;\n    }\n\n    // Daemon up ⇒ hand off to the running broker in place (the brain cycles onto\n    // new bytes immediately; broker survives, REQ-UPD-3). Daemon down ⇒ swap\n    // daemonless and never boot the old broker — the next start runs the new\n    // bytes (REQ-UPDATE-ONE-SHOT-FINISH).\n    let outcome = if spt_daemon::is_running() {\n        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\n    } else {\n        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\n    };\n    match outcome {\n        Ok(o) => {\n            let code = report_apply_outcome(o);\n            // Docs land only AFTER a successful binary apply, and their\n            // failure can never change its outcome (ADR-0036 §4).\n            // [impl->REQ-DOCS-RELEASE-ASSET]\n            if code == 0 {\n                land_staged_docs(&cache);\n            }\n            code\n        }\n        Err(e) => {\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\n            1\n        }\n    }\n}\n\n/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\n/// REQ-UPDATE-RESTART-SAFE-SWAP).\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\nenum UpdateLeg {\n    /// `update fetch --apply` — stage + install the core in one step (the\n    /// brain-only cycle: the invoking process survives by construction).\n    FetchApply,\n    /// `update fetch` — stage only; the swap waits for the Finish leg.\n    Fetch,\n    /// `update adapters` — sweep every gh_release adapter.\n    Adapters,\n    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\n    /// spt-hosted session the invoker's PTY dies here, so this leg is always\n    /// planned LAST (everything else has completed by then).\n    Finish,\n}\n\n/// Plan the composite's leg sequence — pure, unit-gated. The invariants:\n/// core before adapters (core-first doctrine); `--core-only` drops the\n/// adapters leg; `--restart` replaces the in-place apply with stage-now\n/// (`Fetch`) + `Finish` LAST (lethal-leg-last, operator-ruled: the whole-daemon\n/// restart must be the final act so every other leg completes from ANY\n/// invoking context, including a hosted session whose PTY the finish kills).\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\nfn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\n    let mut legs = Vec::new();\n    legs.push(if restart {\n        UpdateLeg::Fetch\n    } else {\n        UpdateLeg::FetchApply\n    });\n    if !core_only {\n        legs.push(UpdateLeg::Adapters);\n    }\n    if restart {\n        legs.push(UpdateLeg::Finish);\n    }\n    legs\n}\n\n/// Bare `spt update` — the composite front door (REQ-UPDATE-DEFAULT-COMPOSITE):\n/// core first (`fetch --apply`; an already-current core no-ops and the run\n/// becomes adapters-only), then `update adapters`. The invoker survives the\n/// core leg by construction — apply cycles the BRAIN only, the broker and every\n/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n///\n/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\n/// ABORTS the remaining legs — updating adapters against a channel that just\n/// refused is compounding, and a `--restart` finish after a failed fetch would\n/// bounce the daemon for nothing. An ADAPTERS-leg failure is isolated (that is\n/// its contract): loud, nonzero at exit, but the composite proceeds — under\n/// `--restart` the finish still completes the core cycle. The accepted\n/// asymmetry (new adapters briefly on old-activated core if the finish itself\n/// fails) is loud and operator-attended by nature of the flag.\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\nfn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n    let mut worst = 0;\n    for leg in plan_update_legs(core_only, restart) {\n        let code = match leg {\n            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\n            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\n            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\n            UpdateLeg::Finish => cmd_update_apply(true),\n        };\n        if code != 0 {\n            if composite_abort_on_failure(leg) {\n                eprintln!(\n                    \"UPDATE_COMPOSITE_ABORTED: the core update failed — the remaining \\\n                     leg(s) were not run\"\n                );\n                return code;\n            }\n            worst = code;\n        }\n    }\n    worst\n}\n\n/// Whether a failed composite leg aborts the remaining legs — pure, unit-gated.\n/// Only the CORE legs abort (a refused channel poisons the adapters leg, and a\n/// finish after a failed fetch is a pointless daemon bounce); an adapters-leg\n/// failure is isolated by contract and the composite proceeds.\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\nfn composite_abort_on_failure(leg: UpdateLeg) -> bool {\n    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\n}\n\n/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\n/// version-matched to the just-applied release (ADR-0036 §4). Extraction is a\n/// bounded system `tar` (the REQ-INSTALL-9 archive precedent) into `docs.new`,\n/// then a swap so a torn extract never leaves a half-tree at `docs`. EVERY\n/// failure degrades to the machine-greppable `UPDATE_DOCS_SKIPPED` on stderr\n/// and leaves the staged bundle in place for the next fetch/apply to retry;\n/// the binary-apply outcome is decided before this runs and never changes.\n// [impl->REQ-DOCS-RELEASE-ASSET]\nfn land_staged_docs(cache: &spt_daemon::ReleaseCache) {\n    let Some(bundle) = cache.staged_docs() else {\n        return; // docs-less release or docs fetch skipped — nothing to land\n    };\n    // Re-verify against the SIGNED staged metadata before touching disk (the\n    // cache is trusted-on-stage, but re-hashing 1 file is cheap insurance).\n    let verified = match cache.staged_update() {\n        Some(spt_daemon::StagedUpdate::Set(signed)) => {\n            serde_json::from_str::<spt_daemon::UpdateSetMetadata>(&signed.metadata_json)\n                .ok()\n                .map(|meta| spt_daemon::verify_update_set_docs(&meta, &bundle).is_ok())\n        }\n        _ => None,\n    };\n    if verified != Some(true) {\n        eprintln!(\n            \"UPDATE_DOCS_SKIPPED: staged docs bundle failed re-verification against the \\\n             signed set — docs retry next fetch\"\n        );\n        return;\n    }\n    let docs_dir = perch::spt_home().join(\"docs\");\n    let staging = perch::spt_home().join(\"docs.new\");\n    let bundle_path = cache.docs_bundle_path();\n    let _ = std::fs::remove_dir_all(&staging);\n    if let Err(e) = std::fs::create_dir_all(&staging) {\n        eprintln!(\"UPDATE_DOCS_SKIPPED: create {}: {e} — docs retry next fetch\", staging.display());\n        return;\n    }\n    let mut keys = std::collections::BTreeMap::new();\n    keys.insert(\n        \"bundle\".to_string(),\n        bundle_path.to_string_lossy().into_owned(),\n    );\n    keys.insert(\"dest\".to_string(), staging.to_string_lossy().into_owned());\n    match spt_runtime::run_bounded_command(\n        \"tar -xzf {bundle} -C {dest}\",\n        &keys,\n        Duration::from_secs(120),\n        None,\n    ) {\n        Ok(out) if out.success() => {}\n        Ok(out) => {\n            let _ = std::fs::remove_dir_all(&staging);\n            eprintln!(\n                \"UPDATE_DOCS_SKIPPED: tar exit {:?}: {} — docs retry next fetch\",\n                out.status_code,\n                out.stderr.trim()\n            );\n            return;\n        }\n        Err(e) => {\n            let _ = std::fs::remove_dir_all(&staging);\n            eprintln!(\"UPDATE_DOCS_SKIPPED: tar: {e} — docs retry next fetch\");\n            return;\n        }\n    }\n    // Swap: retire the old tree, promote the new one. `docs.old` is removed\n    // best-effort afterwards (a pinned handle leaves litter, never a failure).\n    let old = perch::spt_home().join(\"docs.old\");\n    let _ = std::fs::remove_dir_all(&old);\n    if docs_dir.exists() {\n        if let Err(e) = std::fs::rename(&docs_dir, &old) {\n            let _ = std::fs::remove_dir_all(&staging);\n            eprintln!(\n                \"UPDATE_DOCS_SKIPPED: retire old docs: {e} — docs retry next fetch\"\n            );\n            return;\n        }\n    }\n    if let Err(e) = std::fs::rename(&staging, &docs_dir) {\n        // Roll the old tree back so the node keeps its previous docs.\n        let _ = std::fs::rename(&old, &docs_dir);\n        let _ = std::fs::remove_dir_all(&staging);\n        eprintln!(\"UPDATE_DOCS_SKIPPED: promote docs: {e} — docs retry next fetch\");\n        return;\n    }\n    let _ = std::fs::remove_dir_all(&old);\n    // Landed — the staged bundle has served its purpose.\n    cache.clear_staged_docs();\n    println!(\"UPDATE_DOCS_LANDED: {}\", docs_dir.display());\n}\n\n/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\n/// coordinator running new-agent code), then RESTART the daemon onto them so the\n/// whole node runs the new version in one command. The restart rides\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\n/// online hosted endpoints, so the cycle is not a massacre.\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\nfn cmd_update_apply_finish(\n    cache: &spt_daemon::ReleaseCache,\n    keys: &std::path::Path,\n    exe: &std::path::Path,\n) -> i32 {\n    use spt_daemon::ApplyStagedOutcome as O;\n\n    // 1. Swap the bytes with NO broker touched. A daemonless swap can never leave\n    //    a running old broker paired with new brain bytes.\n    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\n        Ok(O::NoUpdate) => {\n            println!(\"NO_UPDATE\");\n            return 0;\n        }\n        // The proceed set — bytes are (or already were) on disk; go restart onto them.\n        Ok(o @ O::AppliedDaemonless { .. })\n        | Ok(o @ O::AppliedPending { .. })\n        | Ok(o @ O::AlreadyApplied { .. }) => o,\n        // The fail-closed refusals — nothing swapped, do NOT restart.\n        Ok(refused) => return report_apply_outcome(refused),\n        Err(e) => {\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\n            return 1;\n        }\n    };\n\n    // 2. Restart the daemon onto the new bytes. Announce the sessions the cycle\n    //    will bounce (they come back on start), stop if up, WAIT for it to be\n    //    actually down (else `start` would see it \"already running\" and skip the\n    //    swap onto new bytes), then start.\n    if spt_daemon::is_running() {\n        let hosted = live_hosted_session_ids();\n        if !hosted.is_empty() {\n            eprintln!(\n                \"Cycling {} hosted session(s) onto the new version (they come back automatically): {}\",\n                hosted.len(),\n                hosted.join(\", \")\n            );\n        }\n        if let Err(e) = spt_daemon::stop_daemon() {\n            eprintln!(\n                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n            );\n            return 1;\n        }\n        let down_by = std::time::Instant::now() + std::time::Duration::from_secs(10);\n        while spt_daemon::is_running() && std::time::Instant::now() < down_by {\n            std::thread::sleep(std::time::Duration::from_millis(50));\n        }\n        if spt_daemon::is_running() {\n            eprintln!(\n                \"DAEMON_STOP_TIMEOUT: the daemon did not stop within 10s — the new version is on \\\n                 disk; run `spt daemon start` once it stops to finish.\"\n            );\n            return 1;\n        }\n    }\n    match spt_daemon::start_daemon() {\n        Ok(_) => {\n            let (version, product_version, already) = match applied {\n                O::AppliedDaemonless {\n                    version,\n                    product_version,\n                }\n                | O::AppliedPending {\n                    version,\n                    product_version,\n                } => (version, product_version, false),\n                O::AlreadyApplied {\n                    version,\n                    product_version,\n                } => (version, product_version, true),\n                _ => unreachable!(\"only proceed variants reach the restart\"),\n            };\n            println!(\n                \"{}\",\n                render_finish_message(version, &product_version, already)\n            );\n            0\n        }\n        Err(e) => {\n            eprintln!(\n                \"DAEMON_START_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n            );\n            1\n        }\n    }\n}\n\n/// The release-download base URL on the GitHub origin. `tag = None` ⇒ the\n/// `latest` release; otherwise the pinned tag. Pure.\n// [impl->REQ-UPD-8]\nfn origin_download_base(repo: &str, tag: Option<&str>) -> String {\n…\n}\n\n[Showing lines 4874-5238 of 18900. Use :5239 to continue]"}],"details":{"truncation":{"content":"        O::AppliedDaemonless {\r\n            version,\r\n            product_version,\r\n        } => {\r\n            println!(\r\n                \"{}\",\r\n                render_applied_daemonless_message(version, &product_version)\r\n            );\r\n            0\r\n        }\r\n        O::Quarantined { version } => {\r\n            eprintln!(\r\n                \"UPDATE_APPLY_QUARANTINED:v{version} failed readiness on this node and was \\\r\n                 auto-rolled-back — refusing to re-apply it. Stage a newer version; the daemon \\\r\n                 is healthy on the previous one.\"\r\n            );\r\n            1\r\n        }\r\n        O::RefusedClass(class) => {\r\n            eprintln!(\"UPDATE_APPLY_REFUSED:{class:?} (full-swap orchestration is not M5 scope)\");\r\n            1\r\n        }\r\n        O::Rejected(reason) => {\r\n            eprintln!(\"UPDATE_APPLY_REJECTED:{reason}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// `spt update apply [--finish]` — the explicit ack named by the update-consent\r\n/// notif. NO LONGER boots the daemon first (the REQ-UPDATE-ONE-SHOT-FINISH wart):\r\n/// a stopped box used to `ensure_daemon_announced` the OLD broker up just to hand\r\n/// off to it, guaranteeing a mixed old-broker/new-brain pair + a manual bounce.\r\n/// Now: daemon up ⇒ in-place brain handoff; daemon down ⇒ a daemonless swap (next\r\n/// start runs it); `--finish` ⇒ swap then restart the daemon onto the new bytes.\r\n// [impl->REQ-UPD-4]\r\n// [impl->REQ-UPD-3]\r\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\r\nfn cmd_update_apply(finish: bool) -> i32 {\r\n    let exe = match std::env::current_exe() {\r\n        Ok(p) => p,\r\n        Err(e) => {\r\n            eprintln!(\"CURRENT_EXE_FAIL: {e}\");\r\n            return 1;\r\n        }\r\n    };\r\n    let cache = spt_daemon::ReleaseCache::open(&perch::spt_home().join(\"releases\"));\r\n    let keys = perch::identity_dir().join(\"release-keys.json\");\r\n\r\n    if finish {\r\n        let code = cmd_update_apply_finish(&cache, &keys, &exe);\r\n        if code == 0 {\r\n            land_staged_docs(&cache);\r\n        }\r\n        return code;\r\n    }\r\n\r\n    // Daemon up ⇒ hand off to the running broker in place (the brain cycles onto\r\n    // new bytes immediately; broker survives, REQ-UPD-3). Daemon down ⇒ swap\r\n    // daemonless and never boot the old broker — the next start runs the new\r\n    // bytes (REQ-UPDATE-ONE-SHOT-FINISH).\r\n    let outcome = if spt_daemon::is_running() {\r\n        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\r\n    } else {\r\n        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\r\n    };\r\n    match outcome {\r\n        Ok(o) => {\r\n            let code = report_apply_outcome(o);\r\n            // Docs land only AFTER a successful binary apply, and their\r\n            // failure can never change its outcome (ADR-0036 §4).\r\n            // [impl->REQ-DOCS-RELEASE-ASSET]\r\n            if code == 0 {\r\n                land_staged_docs(&cache);\r\n            }\r\n            code\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\r\n/// REQ-UPDATE-RESTART-SAFE-SWAP).\r\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\r\nenum UpdateLeg {\r\n    /// `update fetch --apply` — stage + install the core in one step (the\r\n    /// brain-only cycle: the invoking process survives by construction).\r\n    FetchApply,\r\n    /// `update fetch` — stage only; the swap waits for the Finish leg.\r\n    Fetch,\r\n    /// `update adapters` — sweep every gh_release adapter.\r\n    Adapters,\r\n    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\r\n    /// spt-hosted session the invoker's PTY dies here, so this leg is always\r\n    /// planned LAST (everything else has completed by then).\r\n    Finish,\r\n}\r\n\r\n/// Plan the composite's leg sequence — pure, unit-gated. The invariants:\r\n/// core before adapters (core-first doctrine); `--core-only` drops the\r\n/// adapters leg; `--restart` replaces the in-place apply with stage-now\r\n/// (`Fetch`) + `Finish` LAST (lethal-leg-last, operator-ruled: the whole-daemon\r\n/// restart must be the final act so every other leg completes from ANY\r\n/// invoking context, including a hosted session whose PTY the finish kills).\r\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\r\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\r\nfn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\r\n    let mut legs = Vec::new();\r\n    legs.push(if restart {\r\n        UpdateLeg::Fetch\r\n    } else {\r\n        UpdateLeg::FetchApply\r\n    });\r\n    if !core_only {\r\n        legs.push(UpdateLeg::Adapters);\r\n    }\r\n    if restart {\r\n        legs.push(UpdateLeg::Finish);\r\n    }\r\n    legs\r\n}\r\n\r\n/// Bare `spt update` — the composite front door (REQ-UPDATE-DEFAULT-COMPOSITE):\r\n/// core first (`fetch --apply`; an already-current core no-ops and the run\r\n/// becomes adapters-only), then `update adapters`. The invoker survives the\r\n/// core leg by construction — apply cycles the BRAIN only, the broker and every\r\n/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\r\n/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\r\n/// → `apply --finish` LAST: the one-step safe full-cycle swap.\r\n///\r\n/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\r\n/// ABORTS the remaining legs — updating adapters against a channel that just\r\n/// refused is compounding, and a `--restart` finish after a failed fetch would\r\n/// bounce the daemon for nothing. An ADAPTERS-leg failure is isolated (that is\r\n/// its contract): loud, nonzero at exit, but the composite proceeds — under\r\n/// `--restart` the finish still completes the core cycle. The accepted\r\n/// asymmetry (new adapters briefly on old-activated core if the finish itself\r\n/// fails) is loud and operator-attended by nature of the flag.\r\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\r\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\r\nfn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\r\n    let mut worst = 0;\r\n    for leg in plan_update_legs(core_only, restart) {\r\n        let code = match leg {\r\n            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\r\n            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\r\n            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\r\n            UpdateLeg::Finish => cmd_update_apply(true),\r\n        };\r\n        if code != 0 {\r\n            if composite_abort_on_failure(leg) {\r\n                eprintln!(\r\n                    \"UPDATE_COMPOSITE_ABORTED: the core update failed — the remaining \\\r\n                     leg(s) were not run\"\r\n                );\r\n                return code;\r\n            }\r\n            worst = code;\r\n        }\r\n    }\r\n    worst\r\n}\r\n\r\n/// Whether a failed composite leg aborts the remaining legs — pure, unit-gated.\r\n/// Only the CORE legs abort (a refused channel poisons the adapters leg, and a\r\n/// finish after a failed fetch is a pointless daemon bounce); an adapters-leg\r\n/// failure is isolated by contract and the composite proceeds.\r\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\r\nfn composite_abort_on_failure(leg: UpdateLeg) -> bool {\r\n    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\r\n}\r\n\r\n/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\r\n/// version-matched to the just-applied release (ADR-0036 §4). Extraction is a\r\n/// bounded system `tar` (the REQ-INSTALL-9 archive precedent) into `docs.new`,\r\n/// then a swap so a torn extract never leaves a half-tree at `docs`. EVERY\r\n/// failure degrades to the machine-greppable `UPDATE_DOCS_SKIPPED` on stderr\r\n/// and leaves the staged bundle in place for the next fetch/apply to retry;\r\n/// the binary-apply outcome is decided before this runs and never changes.\r\n// [impl->REQ-DOCS-RELEASE-ASSET]\r\nfn land_staged_docs(cache: &spt_daemon::ReleaseCache) {\r\n    let Some(bundle) = cache.staged_docs() else {\r\n        return; // docs-less release or docs fetch skipped — nothing to land\r\n    };\r\n    // Re-verify against the SIGNED staged metadata before touching disk (the\r\n    // cache is trusted-on-stage, but re-hashing 1 file is cheap insurance).\r\n    let verified = match cache.staged_update() {\r\n        Some(spt_daemon::StagedUpdate::Set(signed)) => {\r\n            serde_json::from_str::<spt_daemon::UpdateSetMetadata>(&signed.metadata_json)\r\n                .ok()\r\n                .map(|meta| spt_daemon::verify_update_set_docs(&meta, &bundle).is_ok())\r\n        }\r\n        _ => None,\r\n    };\r\n    if verified != Some(true) {\r\n        eprintln!(\r\n            \"UPDATE_DOCS_SKIPPED: staged docs bundle failed re-verification against the \\\r\n             signed set — docs retry next fetch\"\r\n        );\r\n        return;\r\n    }\r\n    let docs_dir = perch::spt_home().join(\"docs\");\r\n    let staging = perch::spt_home().join(\"docs.new\");\r\n    let bundle_path = cache.docs_bundle_path();\r\n    let _ = std::fs::remove_dir_all(&staging);\r\n    if let Err(e) = std::fs::create_dir_all(&staging) {\r\n        eprintln!(\"UPDATE_DOCS_SKIPPED: create {}: {e} — docs retry next fetch\", staging.display());\r\n        return;\r\n    }\r\n    let mut keys = std::collections::BTreeMap::new();\r\n    keys.insert(\r\n        \"bundle\".to_string(),\r\n        bundle_path.to_string_lossy().into_owned(),\r\n    );\r\n    keys.insert(\"dest\".to_string(), staging.to_string_lossy().into_owned());\r\n    match spt_runtime::run_bounded_command(\r\n        \"tar -xzf {bundle} -C {dest}\",\r\n        &keys,\r\n        Duration::from_secs(120),\r\n        None,\r\n    ) {\r\n        Ok(out) if out.success() => {}\r\n        Ok(out) => {\r\n            let _ = std::fs::remove_dir_all(&staging);\r\n            eprintln!(\r\n                \"UPDATE_DOCS_SKIPPED: tar exit {:?}: {} — docs retry next fetch\",\r\n                out.status_code,\r\n                out.stderr.trim()\r\n            );\r\n            return;\r\n        }\r\n        Err(e) => {\r\n            let _ = std::fs::remove_dir_all(&staging);\r\n            eprintln!(\"UPDATE_DOCS_SKIPPED: tar: {e} — docs retry next fetch\");\r\n            return;\r\n        }\r\n    }\r\n    // Swap: retire the old tree, promote the new one. `docs.old` is removed\r\n    // best-effort afterwards (a pinned handle leaves litter, never a failure).\r\n    let old = perch::spt_home().join(\"docs.old\");\r\n    let _ = std::fs::remove_dir_all(&old);\r\n    if docs_dir.exists() {\r\n        if let Err(e) = std::fs::rename(&docs_dir, &old) {\r\n            let _ = std::fs::remove_dir_all(&staging);\r\n            eprintln!(\r\n                \"UPDATE_DOCS_SKIPPED: retire old docs: {e} — docs retry next fetch\"\r\n            );\r\n            return;\r\n        }\r\n    }\r\n    if let Err(e) = std::fs::rename(&staging, &docs_dir) {\r\n        // Roll the old tree back so the node keeps its previous docs.\r\n        let _ = std::fs::rename(&old, &docs_dir);\r\n        let _ = std::fs::remove_dir_all(&staging);\r\n        eprintln!(\"UPDATE_DOCS_SKIPPED: promote docs: {e} — docs retry next fetch\");\r\n        return;\r\n    }\r\n    let _ = std::fs::remove_dir_all(&old);\r\n    // Landed — the staged bundle has served its purpose.\r\n    cache.clear_staged_docs();\r\n    println!(\"UPDATE_DOCS_LANDED: {}\", docs_dir.display());\r\n}\r\n\r\n/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\r\n/// coordinator running new-agent code), then RESTART the daemon onto them so the\r\n/// whole node runs the new version in one command. The restart rides\r\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\r\n/// online hosted endpoints, so the cycle is not a massacre.\r\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\r\nfn cmd_update_apply_finish(\r\n    cache: &spt_daemon::ReleaseCache,\r\n    keys: &std::path::Path,\r\n    exe: &std::path::Path,\r\n) -> i32 {\r\n    use spt_daemon::ApplyStagedOutcome as O;\r\n\r\n    // 1. Swap the bytes with NO broker touched. A daemonless swap can never leave\r\n    //    a running old broker paired with new brain bytes.\r\n    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\r\n        Ok(O::NoUpdate) => {\r\n            println!(\"NO_UPDATE\");\r\n            return 0;\r\n        }\r\n        // The proceed set — bytes are (or already were) on disk; go restart onto them.\r\n        Ok(o @ O::AppliedDaemonless { .. })\r\n        | Ok(o @ O::AppliedPending { .. })\r\n        | Ok(o @ O::AlreadyApplied { .. }) => o,\r\n        // The fail-closed refusals — nothing swapped, do NOT restart.\r\n        Ok(refused) => return report_apply_outcome(refused),\r\n        Err(e) => {\r\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\r\n            return 1;\r\n        }\r\n    };\r\n\r\n    // 2. Restart the daemon onto the new bytes. Announce the sessions the cycle\r\n    //    will bounce (they come back on start), stop if up, WAIT for it to be\r\n    //    actually down (else `start` would see it \"already running\" and skip the\r\n    //    swap onto new bytes), then start.\r\n    if spt_daemon::is_running() {\r\n        let hosted = live_hosted_session_ids();\r\n        if !hosted.is_empty() {\r\n            eprintln!(\r\n                \"Cycling {} hosted session(s) onto the new version (they come back automatically): {}\",\r\n                hosted.len(),\r\n                hosted.join(\", \")\r\n            );\r\n        }\r\n        if let Err(e) = spt_daemon::stop_daemon() {\r\n            eprintln!(\r\n                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\r\n            );\r\n            return 1;\r\n        }\r\n        let down_by = std::time::Instant::now() + std::time::Duration::from_secs(10);\r\n        while spt_daemon::is_running() && std::time::Instant::now() < down_by {\r\n            std::thread::sleep(std::time::Duration::from_millis(50));\r\n        }\r\n        if spt_daemon::is_running() {\r\n            eprintln!(\r\n                \"DAEMON_STOP_TIMEOUT: the daemon did not stop within 10s — the new version is on \\\r\n                 disk; run `spt daemon start` once it stops to finish.\"\r\n            );\r\n            return 1;\r\n        }\r\n    }\r\n    match spt_daemon::start_daemon() {\r\n        Ok(_) => {\r\n            let (version, product_version, already) = match applied {\r\n                O::AppliedDaemonless {\r\n                    version,\r\n                    product_version,\r\n                }\r\n                | O::AppliedPending {\r\n                    version,\r\n                    product_version,\r\n                } => (version, product_version, false),\r\n                O::AlreadyApplied {\r\n                    version,\r\n                    product_version,\r\n                } => (version, product_version, true),\r\n                _ => unreachable!(\"only proceed variants reach the restart\"),\r\n            };\r\n            println!(\r\n                \"{}\",\r\n                render_finish_message(version, &product_version, already)\r\n            );\r\n            0\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\r\n                \"DAEMON_START_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\r\n            );\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// The release-download base URL on the GitHub origin. `tag = None` ⇒ the\r\n/// `latest` release; otherwise the pinned tag. Pure.\r\n// [impl->REQ-UPD-8]\r\nfn origin_download_base(repo: &str, tag: Option<&str>) -> String {\r","truncated":true,"truncatedBy":"lines","totalLines":14027,"totalBytes":15391,"outputLines":365,"outputBytes":15391,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn report_apply_outcome(outcome: spt_daemon::ApplyStagedOutcome) -> i32 {\n…\n    match outcome {\n…\n        O::AppliedDaemonless {\r\n            version,\r\n            product_version,\r\n        } => {\r\n            println!(\r\n                \"{}\",\r\n                render_applied_daemonless_message(version, &product_version)\r\n            );\r\n            0\r\n        }\r\n        O::Quarantined { version } => {\r\n            eprintln!(\r\n                \"UPDATE_APPLY_QUARANTINED:v{version} failed readiness on this node and was \\\r\n                 auto-rolled-back — refusing to re-apply it. Stage a newer version; the daemon \\\r\n                 is healthy on the previous one.\"\r\n            );\r\n            1\r\n        }\r\n        O::RefusedClass(class) => {\r\n            eprintln!(\"UPDATE_APPLY_REFUSED:{class:?} (full-swap orchestration is not M5 scope)\");\r\n            1\r\n        }\r\n        O::Rejected(reason) => {\r\n            eprintln!(\"UPDATE_APPLY_REJECTED:{reason}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// `spt update apply [--finish]` — the explicit ack named by the update-consent\r\n/// notif. NO LONGER boots the daemon first (the REQ-UPDATE-ONE-SHOT-FINISH wart):\r\n/// a stopped box used to `ensure_daemon_announced` the OLD broker up just to hand\r\n/// off to it, guaranteeing a mixed old-broker/new-brain pair + a manual bounce.\r\n/// Now: daemon up ⇒ in-place brain handoff; daemon down ⇒ a daemonless swap (next\r\n/// start runs it); `--finish` ⇒ swap then restart the daemon onto the new bytes.\r\n// [impl->REQ-UPD-4]\r\n// [impl->REQ-UPD-3]\r\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\r\nfn cmd_update_apply(finish: bool) -> i32 {\r\n    let exe = match std::env::current_exe() {\r\n        Ok(p) => p,\r\n        Err(e) => {\r\n            eprintln!(\"CURRENT_EXE_FAIL: {e}\");\r\n            return 1;\r\n        }\r\n    };\r\n    let cache = spt_daemon::ReleaseCache::open(&perch::spt_home().join(\"releases\"));\r\n    let keys = perch::identity_dir().join(\"release-keys.json\");\r\n\r\n    if finish {\r\n        let code = cmd_update_apply_finish(&cache, &keys, &exe);\r\n        if code == 0 {\r\n            land_staged_docs(&cache);\r\n        }\r\n        return code;\r\n    }\r\n\r\n    // Daemon up ⇒ hand off to the running broker in place (the brain cycles onto\r\n    // new bytes immediately; broker survives, REQ-UPD-3). Daemon down ⇒ swap\r\n    // daemonless and never boot the old broker — the next start runs the new\r\n    // bytes (REQ-UPDATE-ONE-SHOT-FINISH).\r\n    let outcome = if spt_daemon::is_running() {\r\n        spt_daemon::apply_staged(&cache, &keys, &exe, &spt_daemon::broker_socket_name())\r\n    } else {\r\n        spt_daemon::apply_staged_daemonless(&cache, &keys, &exe)\r\n    };\r\n    match outcome {\r\n        Ok(o) => {\r\n            let code = report_apply_outcome(o);\r\n            // Docs land only AFTER a successful binary apply, and their\r\n            // failure can never change its outcome (ADR-0036 §4).\r\n            // [impl->REQ-DOCS-RELEASE-ASSET]\r\n            if code == 0 {\r\n                land_staged_docs(&cache);\r\n            }\r\n            code\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\r\n/// REQ-UPDATE-RESTART-SAFE-SWAP).\r\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\r\nenum UpdateLeg {\r\n    /// `update fetch --apply` — stage + install the core in one step (the\r\n    /// brain-only cycle: the invoking process survives by construction).\r\n    FetchApply,\r\n    /// `update fetch` — stage only; the swap waits for the Finish leg.\r\n    Fetch,\r\n    /// `update adapters` — sweep every gh_release adapter.\r\n    Adapters,\r\n    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\r\n    /// spt-hosted session the invoker's PTY dies here, so this leg is always\r\n    /// planned LAST (everything else has completed by then).\r\n    Finish,\r\n}\r\n\r\n/// Plan the composite's leg sequence — pure, unit-gated. The invariants:\r\n/// core before adapters (core-first doctrine); `--core-only` drops the\r\n/// adapters leg; `--restart` replaces the in-place apply with stage-now\r\n/// (`Fetch`) + `Finish` LAST (lethal-leg-last, operator-ruled: the whole-daemon\r\n/// restart must be the final act so every other leg completes from ANY\r\n/// invoking context, including a hosted session whose PTY the finish kills).\r\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\r\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\r\nfn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\r\n    let mut legs = Vec::new();\r\n    legs.push(if restart {\r\n        UpdateLeg::Fetch\r\n    } else {\r\n        UpdateLeg::FetchApply\r\n    });\r\n    if !core_only {\r\n        legs.push(UpdateLeg::Adapters);\r\n    }\r\n    if restart {\r\n        legs.push(UpdateLeg::Finish);\r\n    }\r\n    legs\r\n}\r\n\r\n/// Bare `spt update` — the composite front door (REQ-UPDATE-DEFAULT-COMPOSITE):\r\n/// core first (`fetch --apply`; an already-current core no-ops and the run\r\n/// becomes adapters-only), then `update adapters`. The invoker survives the\r\n/// core leg by construction — apply cycles the BRAIN only, the broker and every\r\n/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\r\n/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\r\n/// → `apply --finish` LAST: the one-step safe full-cycle swap.\r\n///\r\n/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\r\n/// ABORTS the remaining legs — updating adapters against a channel that just\r\n/// refused is compounding, and a `--restart` finish after a failed fetch would\r\n/// bounce the daemon for nothing. An ADAPTERS-leg failure is isolated (that is\r\n/// its contract): loud, nonzero at exit, but the composite proceeds — under\r\n/// `--restart` the finish still completes the core cycle. The accepted\r\n/// asymmetry (new adapters briefly on old-activated core if the finish itself\r\n/// fails) is loud and operator-attended by nature of the flag.\r\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\r\n// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\r\nfn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\r\n    let mut worst = 0;\r\n    for leg in plan_update_legs(core_only, restart) {\r\n        let code = match leg {\r\n            UpdateLeg::FetchApply => cmd_update_fetch(None, None, true),\r\n            UpdateLeg::Fetch => cmd_update_fetch(None, None, false),\r\n            UpdateLeg::Adapters => cmd_adapter_update(&spt_store::perch::adapters_dir(), None),\r\n            UpdateLeg::Finish => cmd_update_apply(true),\r\n        };\r\n        if code != 0 {\r\n            if composite_abort_on_failure(leg) {\r\n                eprintln!(\r\n                    \"UPDATE_COMPOSITE_ABORTED: the core update failed — the remaining \\\r\n                     leg(s) were not run\"\r\n                );\r\n                return code;\r\n            }\r\n            worst = code;\r\n        }\r\n    }\r\n    worst\r\n}\r\n\r\n/// Whether a failed composite leg aborts the remaining legs — pure, unit-gated.\r\n/// Only the CORE legs abort (a refused channel poisons the adapters leg, and a\r\n/// finish after a failed fetch is a pointless daemon bounce); an adapters-leg\r\n/// failure is isolated by contract and the composite proceeds.\r\n// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\r\nfn composite_abort_on_failure(leg: UpdateLeg) -> bool {\r\n    matches!(leg, UpdateLeg::FetchApply | UpdateLeg::Fetch)\r\n}\r\n\r\n/// Land the staged docs bundle at `$SPT_HOME/docs` — the single current copy,\r\n/// version-matched to the just-applied release (ADR-0036 §4). Extraction is a\r\n/// bounded system `tar` (the REQ-INSTALL-9 archive precedent) into `docs.new`,\r\n/// then a swap so a torn extract never leaves a half-tree at `docs`. EVERY\r\n/// failure degrades to the machine-greppable `UPDATE_DOCS_SKIPPED` on stderr\r\n/// and leaves the staged bundle in place for the next fetch/apply to retry;\r\n/// the binary-apply outcome is decided before this runs and never changes.\r\n// [impl->REQ-DOCS-RELEASE-ASSET]\r\nfn land_staged_docs(cache: &spt_daemon::ReleaseCache) {\r\n    let Some(bundle) = cache.staged_docs() else {\r\n        return; // docs-less release or docs fetch skipped — nothing to land\r\n    };\r\n    // Re-verify against the SIGNED staged metadata before touching disk (the\r\n    // cache is trusted-on-stage, but re-hashing 1 file is cheap insurance).\r\n    let verified = match cache.staged_update() {\r\n        Some(spt_daemon::StagedUpdate::Set(signed)) => {\r\n            serde_json::from_str::<spt_daemon::UpdateSetMetadata>(&signed.metadata_json)\r\n                .ok()\r\n                .map(|meta| spt_daemon::verify_update_set_docs(&meta, &bundle).is_ok())\r\n        }\r\n        _ => None,\r\n    };\r\n    if verified != Some(true) {\r\n        eprintln!(\r\n            \"UPDATE_DOCS_SKIPPED: staged docs bundle failed re-verification against the \\\r\n             signed set — docs retry next fetch\"\r\n        );\r\n        return;\r\n    }\r\n    let docs_dir = perch::spt_home().join(\"docs\");\r\n    let staging = perch::spt_home().join(\"docs.new\");\r\n    let bundle_path = cache.docs_bundle_path();\r\n    let _ = std::fs::remove_dir_all(&staging);\r\n    if let Err(e) = std::fs::create_dir_all(&staging) {\r\n        eprintln!(\"UPDATE_DOCS_SKIPPED: create {}: {e} — docs retry next fetch\", staging.display());\r\n        return;\r\n    }\r\n    let mut keys = std::collections::BTreeMap::new();\r\n    keys.insert(\r\n        \"bundle\".to_string(),\r\n        bundle_path.to_string_lossy().into_owned(),\r\n    );\r\n    keys.insert(\"dest\".to_string(), staging.to_string_lossy().into_owned());\r\n    match spt_runtime::run_bounded_command(\r\n        \"tar -xzf {bundle} -C {dest}\",\r\n        &keys,\r\n        Duration::from_secs(120),\r\n        None,\r\n    ) {\r\n        Ok(out) if out.success() => {}\r\n        Ok(out) => {\r\n            let _ = std::fs::remove_dir_all(&staging);\r\n            eprintln!(\r\n                \"UPDATE_DOCS_SKIPPED: tar exit {:?}: {} — docs retry next fetch\",\r\n                out.status_code,\r\n                out.stderr.trim()\r\n            );\r\n            return;\r\n        }\r\n        Err(e) => {\r\n            let _ = std::fs::remove_dir_all(&staging);\r\n            eprintln!(\"UPDATE_DOCS_SKIPPED: tar: {e} — docs retry next fetch\");\r\n            return;\r\n        }\r\n    }\r\n    // Swap: retire the old tree, promote the new one. `docs.old` is removed\r\n    // best-effort afterwards (a pinned handle leaves litter, never a failure).\r\n    let old = perch::spt_home().join(\"docs.old\");\r\n    let _ = std::fs::remove_dir_all(&old);\r\n    if docs_dir.exists() {\r\n        if let Err(e) = std::fs::rename(&docs_dir, &old) {\r\n            let _ = std::fs::remove_dir_all(&staging);\r\n            eprintln!(\r\n                \"UPDATE_DOCS_SKIPPED: retire old docs: {e} — docs retry next fetch\"\r\n            );\r\n            return;\r\n        }\r\n    }\r\n    if let Err(e) = std::fs::rename(&staging, &docs_dir) {\r\n        // Roll the old tree back so the node keeps its previous docs.\r\n        let _ = std::fs::rename(&old, &docs_dir);\r\n        let _ = std::fs::remove_dir_all(&staging);\r\n        eprintln!(\"UPDATE_DOCS_SKIPPED: promote docs: {e} — docs retry next fetch\");\r\n        return;\r\n    }\r\n    let _ = std::fs::remove_dir_all(&old);\r\n    // Landed — the staged bundle has served its purpose.\r\n    cache.clear_staged_docs();\r\n    println!(\"UPDATE_DOCS_LANDED: {}\", docs_dir.display());\r\n}\r\n\r\n/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\r\n/// coordinator running new-agent code), then RESTART the daemon onto them so the\r\n/// whole node runs the new version in one command. The restart rides\r\n/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\r\n/// online hosted endpoints, so the cycle is not a massacre.\r\n// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\r\nfn cmd_update_apply_finish(\r\n    cache: &spt_daemon::ReleaseCache,\r\n    keys: &std::path::Path,\r\n    exe: &std::path::Path,\r\n) -> i32 {\r\n    use spt_daemon::ApplyStagedOutcome as O;\r\n\r\n    // 1. Swap the bytes with NO broker touched. A daemonless swap can never leave\r\n    //    a running old broker paired with new brain bytes.\r\n    let applied = match spt_daemon::apply_staged_daemonless(cache, keys, exe) {\r\n        Ok(O::NoUpdate) => {\r\n            println!(\"NO_UPDATE\");\r\n            return 0;\r\n        }\r\n        // The proceed set — bytes are (or already were) on disk; go restart onto them.\r\n        Ok(o @ O::AppliedDaemonless { .. })\r\n        | Ok(o @ O::AppliedPending { .. })\r\n        | Ok(o @ O::AlreadyApplied { .. }) => o,\r\n        // The fail-closed refusals — nothing swapped, do NOT restart.\r\n        Ok(refused) => return report_apply_outcome(refused),\r\n        Err(e) => {\r\n            eprintln!(\"UPDATE_APPLY_FAIL:{e}\");\r\n            return 1;\r\n        }\r\n    };\r\n\r\n    // 2. Restart the daemon onto the new bytes. Announce the sessions the cycle\r\n    //    will bounce (they come back on start), stop if up, WAIT for it to be\r\n    //    actually down (else `start` would see it \"already running\" and skip the\r\n    //    swap onto new bytes), then start.\r\n    if spt_daemon::is_running() {\r\n        let hosted = live_hosted_session_ids();\r\n        if !hosted.is_empty() {\r\n            eprintln!(\r\n                \"Cycling {} hosted session(s) onto the new version (they come back automatically): {}\",\r\n                hosted.len(),\r\n                hosted.join(\", \")\r\n            );\r\n        }\r\n        if let Err(e) = spt_daemon::stop_daemon() {\r\n            eprintln!(\r\n                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\r\n            );\r\n            return 1;\r\n        }\r\n        let down_by = std::time::Instant::now() + std::time::Duration::from_secs(10);\r\n        while spt_daemon::is_running() && std::time::Instant::now() < down_by {\r\n            std::thread::sleep(std::time::Duration::from_millis(50));\r\n        }\r\n        if spt_daemon::is_running() {\r\n            eprintln!(\r\n                \"DAEMON_STOP_TIMEOUT: the daemon did not stop within 10s — the new version is on \\\r\n                 disk; run `spt daemon start` once it stops to finish.\"\r\n            );\r\n            return 1;\r\n        }\r\n    }\r\n    match spt_daemon::start_daemon() {\r\n        Ok(_) => {\r\n            let (version, product_version, already) = match applied {\r\n                O::AppliedDaemonless {\r\n                    version,\r\n                    product_version,\r\n                }\r\n                | O::AppliedPending {\r\n                    version,\r\n                    product_version,\r\n                } => (version, product_version, false),\r\n                O::AlreadyApplied {\r\n                    version,\r\n                    product_version,\r\n                } => (version, product_version, true),\r\n                _ => unreachable!(\"only proceed variants reach the restart\"),\r\n            };\r\n            println!(\r\n                \"{}\",\r\n                render_finish_message(version, &product_version, already)\r\n            );\r\n            0\r\n        }\r\n        Err(e) => {\r\n            eprintln!(\r\n                \"DAEMON_START_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\r\n            );\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// The release-download base URL on the GitHub origin. `tag = None` ⇒ the\r\n/// `latest` release; otherwise the pinned tag. Pure.\r\n// [impl->REQ-UPD-8]\r\nfn origin_download_base(repo: &str, tag: Option<&str>) -> String {\r\n…\n}","startLine":4848,"lineNumbers":[4848,null,4850,null,4874,4875,4876,4877,4878,4879,4880,4881,4882,4883,4884,4885,4886,4887,4888,4889,4890,4891,4892,4893,4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4908,4909,4910,4911,4912,4913,4914,4915,4916,4917,4918,4919,4920,4921,4922,4923,4924,4925,4926,4927,4928,4929,4930,4931,4932,4933,4934,4935,4936,4937,4938,4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4950,4951,4952,4953,4954,4955,4956,4957,4958,4959,4960,4961,4962,4963,4964,4965,4966,4967,4968,4969,4970,4971,4972,4973,4974,4975,4976,4977,4978,4979,4980,4981,4982,4983,4984,4985,4986,4987,4988,4989,4990,4991,4992,4993,4994,4995,4996,4997,4998,4999,5000,5001,5002,5003,5004,5005,5006,5007,5008,5009,5010,5011,5012,5013,5014,5015,5016,5017,5018,5019,5020,5021,5022,5023,5024,5025,5026,5027,5028,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,5040,5041,5042,5043,5044,5045,5046,5047,5048,5049,5050,5051,5052,5053,5054,5055,5056,5057,5058,5059,5060,5061,5062,5063,5064,5065,5066,5067,5068,5069,5070,5071,5072,5073,5074,5075,5076,5077,5078,5079,5080,5081,5082,5083,5084,5085,5086,5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102,5103,5104,5105,5106,5107,5108,5109,5110,5111,5112,5113,5114,5115,5116,5117,5118,5119,5120,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133,5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149,5150,5151,5152,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164,5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,5176,5177,5178,5179,5180,5181,5182,5183,5184,5185,5186,5187,5188,5189,5190,5191,5192,5193,5194,5195,5196,5197,5198,5199,5200,5201,5202,5203,5204,5205,5206,5207,5208,5209,5210,5211,5212,5213,5214,5215,5216,5217,5218,5219,5220,5221,5222,5223,5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,5237,5238,null,5243]},"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":15391,"outputLines":365,"outputBytes":15391,"shownRange":{"start":4874,"end":5238},"nextOffset":5239}}},"isError":false,"timestamp":1784195829945}}
{"type":"message","id":"45cf993a","parentId":"a0eda30d","timestamp":"2026-07-16T09:57:30.438Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing inheritance and connection topology**"},{"type":"toolCall","id":"call_R2IVDmEr83xCSfb6D1BYmuv6|fc_0698befac979932d016a58ab089de8819a9d4f8e02817b8950","name":"read","arguments":{"i":"Reading supervisor planned kill window","path":"crates/spt-daemon/src/brainproc.rs","selector":"840-970,1090-1140"}},{"type":"toolCall","id":"call_4O2r8DTdygQztN2Y6B6mv0BF|fc_0698befac979932d016a58ab089e2c819a961a2686f01297e1","name":"read","arguments":{"i":"Reading daemon supervisor shutdown","path":"crates/spt-daemon/src/daemon.rs","selector":"300-375,610-725"}},{"type":"toolCall","id":"call_OnwsyNmETXk5I22mnT8NT2bT|fc_0698befac979932d016a58ab089e40819a8f97a2df3801716a","name":"read","arguments":{"i":"Reading local attach service lifecycle","path":"crates/spt-daemon/src/attach.rs","selector":"1-115,170-360,450-625"}},{"type":"toolCall","id":"call_guFv0r25aSiiM8F2AjJzgCRV|fc_0698befac979932d016a58ab089e88819ab3ceb99594ad8138","name":"read","arguments":{"i":"Reading rc topology and pump","path":"crates/spt/src/rc.rs","selector":"1-50,850-950,1080-1280,1280-1475"}},{"type":"toolCall","id":"call_FxEksntciaPYsBPLCt7tPVm9|fc_0698befac979932d016a58ab089e98819abab516c415ae4731","name":"grep","arguments":{"i":"Finding update finish survival tests","pattern":"UPDATE-FINISH-ENDPOINT-SURVIVAL|apply --finish|finish.*surviv|restart.*endpoint|resume_restart_orphaned_endpoints|UPDATE-RESTART-SAFE-SWAP","path":"crates/spt/tests;crates/spt-daemon/tests;crates/spt/src/cli.rs;crates/spt-daemon/src/livehost.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_gFizVNRTuo4yRTaG3lEX81ps|fc_0698befac979932d016a58ab089ea4819aa79d5e19c7466b9f","name":"grep","arguments":{"i":"Finding brain restart test coverage","pattern":"planned restart|request_brain_restart|BrainRestart|StartReason::Update|old_gen_drained|refresh_brain","path":"crates/spt-daemon/src;crates/spt-daemon/tests;crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_qOw333m6LciF6inARE8v1HFB|fc_0698befac979932d016a58ab089f2c819a9c4aa0e6e325c767","name":"grep","arguments":{"i":"Finding controller poison tests","pattern":"non_draining_controller|poisoned conn|gate timeout|stall_evict|black.hole|detached.*reattach|resume.*controller","path":"crates/spt-daemon/tests;crates/spt-daemon/src;crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":34722,"output":613,"cacheRead":44544,"cacheWrite":0,"totalTokens":79879,"reasoningTokens":123,"cost":{"input":0.17361000000000001,"output":0.01839,"cacheRead":0.022272,"cacheWrite":0,"total":0.21427200000000002}},"stopReason":"toolUse","timestamp":1784195829991,"responseId":"resp_0698befac979932d016a58aaf67918819aaa2ba90ef23c7c85","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58aafb6570819abfcc42b9fc4db76c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsI3sT1l_m3HmJ9qxL_GDyEuBX8zdu_5jjcPkbtgGr-2kpneeFEHHoci9zfk1ZE-BuhRzEdimrMP2weo3dgRUIedRYwN4g7_xAcjwwsvDOQdxVmfkfDVkB8IwstX9DS7GiWqcCxbLW_wihILaesbQoH7ed6ZqLHutyK0eTe8FOjbOvHLmgfrrMSIyIv3e6R82Mry2rLdIQ8OgyGT_FPNUV_hTbDOirZLDa9cuxIBLsUQfKGOFnVlvZgZH7YCL5pH0L2e_hWY2oJwzCqQZ06uoCZOS3lhY_jy_ymhTvT--bnFabkM4Nks0xWBGXXinRRiTWPldgMyZPT0AalJgPeYEaQv1mCTWzm6axTEVetmb5GaE3ZjyalhrR3Da6JeWMcGSOB0J-996iPr5y7fZCVwMmILWFllXYKTVhah2WAbNv6o2cg_cOx5peVM1T3ckeKO8E-uQMXMAGKBhYxJBMrP5WKNue7Ms0dZs0JrC3SWG9DXclsmUnuEuuLcs_btaWxAh77RUA1k72p5OlzUu1oW-ySVM7FT3L_VJiYgf1bIjHkbyH4UQd7VXI3VXBNBxMPdL2Li-1s7kEAPb0TPTzVdKJU2xjLYtgCClsdbWWRyLmUZlNqdznka-3_MWpDlWwl4sLY8TOsa38S6XxGMKX130F_br_HXWezkMzE0n0JS5ymxIXucosHiP-Cvj3C5wHMrRZ3tHg79Ld1Dz4lDYZB-JHV9VE2Hfjidk-RE8BRccUE285nQ7YXkdzmuhP4UdsKIBFqCtN8levyuTBTUxc93Ei2Hsx9fwd3Ix4T6d7DcSlppITj4RJmgTq36X_OMSjZv2nhKUjN0Oxv2bIKqMoBe6bV7PGIBMLASDRyYvoAIHnGbJxy8OdhSqutkm8zDhRXtg0daTqlwnShpsFm-pEgYfRCqOTcSRKGxqxLz3AOud4NOzM0PdyCeXeDOW8TxT7Qg_PrZOtoe-8PKheZtfbJyG9ha0ovTVGYxbiHsUtcJLn3xRb8OccBzJL-bYbxausSIs9nqPDKVLWLnILMCYPlrVtuC2TLq5P_AtLHuJkTsxqWar0WcFsZX0XYxRe7j8Trm4I2Dh9zi30UJTi7GrLU2FG7ImAdbsM-rskYF-_nXfIWaZe1u_kQrzaXThzxd7G-JWwa-gWzLGh7FEi927JeW4IOMY42szEC4jzp_jkr_aBWgh6eSi7B0aCJCW2mnLrHKuF3Kl45HcXkVYuu6DYoIZdy16M-Vwg6dQZTlnV9ANBVo-NCbNdQENMAJck4Nl2JKgnB17eDsGG9MJwbM3QeglbBGrpfCQG-Ay4JifBRJRh-oqSjTiwtnaGSECpqRbjlJDp9qMgczc0ggqh-bAKcZQXixoUn_CYm1yV7hc4HQSvEECbhxrLJC1pH3VKnGAVvB5DYZggi-OUNEYSoaAXKwHlvG-BNklX-jjGgI9zWwxL15zXVdKro4DHsqZ4OODdJpkg2Xs77B04r_Fp90XIDiV2LA44Rg2n62CFfLexrs9LbEerMJUmPK0vSWnLKIXU9udSpwbfAVKkJhRQndHMFVDWsFc5Hdvv5gdgJgReIcX_2sJ9nSiMykoi67lkeSE1CY0RkCheAFVkX7anMJ9HjY4ZunpeUj6kyO_1WCCZpRJZL4ay9nNO8dCREyuKw7LEyD2U183FZOw92fBGv8Oevxs7-GfQ4-kB_cVpk8265cTYdOxme-vyhYVWLaPkQyrSLVGs4kc_E4d2fCoBmWCDEfzNuqGU5hpkvYSyPTNlYPmKd1FrCwZchjkxQXbYMtkpPfXl0","summary":[{"type":"summary_text","text":"**Analyzing inheritance and connection topology**"}]},{"id":"fc_0698befac979932d016a58ab089de8819a9d4f8e02817b8950","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading supervisor planned kill window\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"840-970,1090-1140\"}","call_id":"call_R2IVDmEr83xCSfb6D1BYmuv6","name":"read"},{"id":"fc_0698befac979932d016a58ab089e2c819a961a2686f01297e1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon supervisor shutdown\",\"path\":\"crates/spt-daemon/src/daemon.rs\",\"selector\":\"300-375,610-725\"}","call_id":"call_4O2r8DTdygQztN2Y6B6mv0BF","name":"read"},{"id":"fc_0698befac979932d016a58ab089e40819a8f97a2df3801716a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading local attach service lifecycle\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"1-115,170-360,450-625\"}","call_id":"call_OnwsyNmETXk5I22mnT8NT2bT","name":"read"},{"id":"fc_0698befac979932d016a58ab089e88819ab3ceb99594ad8138","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc topology and pump\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1-50,850-950,1080-1280,1280-1475\"}","call_id":"call_guFv0r25aSiiM8F2AjJzgCRV","name":"read"},{"id":"fc_0698befac979932d016a58ab089e98819abab516c415ae4731","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding update finish survival tests\",\"pattern\":\"UPDATE-FINISH-ENDPOINT-SURVIVAL|apply --finish|finish.*surviv|restart.*endpoint|resume_restart_orphaned_endpoints|UPDATE-RESTART-SAFE-SWAP\",\"path\":\"crates/spt/tests;crates/spt-daemon/tests;crates/spt/src/cli.rs;crates/spt-daemon/src/livehost.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_FxEksntciaPYsBPLCt7tPVm9","name":"grep"},{"id":"fc_0698befac979932d016a58ab089ea4819aa79d5e19c7466b9f","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding brain restart test coverage\",\"pattern\":\"planned restart|request_brain_restart|BrainRestart|StartReason::Update|old_gen_drained|refresh_brain\",\"path\":\"crates/spt-daemon/src;crates/spt-daemon/tests;crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_gFizVNRTuo4yRTaG3lEX81ps","name":"grep"},{"id":"fc_0698befac979932d016a58ab089f2c819a9c4aa0e6e325c767","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding controller poison tests\",\"pattern\":\"non_draining_controller|poisoned conn|gate timeout|stall_evict|black.hole|detached.*reattach|resume.*controller\",\"path\":\"crates/spt-daemon/tests;crates/spt-daemon/src;crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_qOw333m6LciF6inARE8v1HFB","name":"grep"}]},"duration":20445.474799999967,"ttft":5381.214900000021,"contextSnapshot":{"promptTokens":79266,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_R2IVDmEr83xCSfb6D1BYmuv6|fc_0698befac979932d016a58ab089de8819a9d4f8e02817b8950","toolName":"read","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading supervisor planned kill window"},"id":"51825ff2","parentId":"45cf993a","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4O2r8DTdygQztN2Y6B6mv0BF|fc_0698befac979932d016a58ab089e2c819a961a2686f01297e1","toolName":"read","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt-daemon/src/daemon.rs"},"intent":"Reading daemon supervisor shutdown"},"id":"b585c45e","parentId":"51825ff2","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OnwsyNmETXk5I22mnT8NT2bT|fc_0698befac979932d016a58ab089e40819a8f97a2df3801716a","toolName":"read","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading local attach service lifecycle"},"id":"b7760f32","parentId":"b585c45e","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_guFv0r25aSiiM8F2AjJzgCRV|fc_0698befac979932d016a58ab089e88819ab3ceb99594ad8138","toolName":"read","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc topology and pump"},"id":"d6258abf","parentId":"b7760f32","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FxEksntciaPYsBPLCt7tPVm9|fc_0698befac979932d016a58ab089e98819abab516c415ae4731","toolName":"grep","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt/tests;crates/spt-daemon/tests;crates/spt/src/cli.rs;crates/spt-daemon/src/livehost.rs"},"intent":"Finding update finish survival tests"},"id":"9d3bc48b","parentId":"d6258abf","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gFizVNRTuo4yRTaG3lEX81ps|fc_0698befac979932d016a58ab089ea4819aa79d5e19c7466b9f","toolName":"grep","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt-daemon/src;crates/spt-daemon/tests;crates/spt/tests"},"intent":"Finding brain restart test coverage"},"id":"04d355ba","parentId":"9d3bc48b","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qOw333m6LciF6inARE8v1HFB|fc_0698befac979932d016a58ab089f2c819a9c4aa0e6e325c767","toolName":"grep","startedAt":"2026-07-16T09:57:30.438Z","args":{"path":"crates/spt-daemon/tests;crates/spt-daemon/src;crates/spt/tests"},"intent":"Finding controller poison tests"},"id":"5bc2bf61","parentId":"04d355ba","timestamp":"2026-07-16T09:57:30.438Z"}
{"type":"message","id":"4d9a3bee","parentId":"5bc2bf61","timestamp":"2026-07-16T09:57:30.467Z","message":{"role":"toolResult","toolCallId":"call_4O2r8DTdygQztN2Y6B6mv0BF|fc_0698befac979932d016a58ab089e2c819a961a2686f01297e1","toolName":"read","content":[{"type":"text","text":"    pub fn run() -> io::Result<()> {\n…\n        if let Some(broker) = &broker_handle {\n…\n            thread::spawn(move || {\n                let _ = crate::tunnelhub::serve_tunnel_control(\n…\n                );\n            });\n        }\n\n        // Shell wake-watchers (M5-D4b) — the offline half of the shell\n        // online/offline exclusivity — now run in the BRAIN child (restoration\n        // D2-2): shellwake is brain-owned logic (ADR-0018 Q5 exception), a pure\n        // disk-reconciler whose boot sweep re-reconciles the watcher children on\n        // every brain start. The broker no longer hosts it; see\n        // [`crate::brainproc::run_brain`].\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\n        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n        // restoration): the broker is the always-up per-machine anchor; it spawns\n        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\n        // makes an update a *planned* restart on this same path: the supervisor's\n        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n        // onto the new binary with `start-reason=update`. The stop flag is held\n        // for symmetry, never raised (the daemon runs until killed). The brain\n        // child inherits this process's already-unelevated token.\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n        // container the supervisor enrolls every (re)spawned brain into. Created\n        // here so it outlives the supervisor and is reachable from the\n        // graceful-stop path below. The brain subtree (the brain + its shellwake\n        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n        // never in the container.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        let reaper = crate::reap::BrainReaper::new();\n        // The supervisor stop flag — hoisted out of the wiring block (was held\n        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n        // it, so the supervisor does not respawn the brain we are about to reap.\n        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        {\n            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n                Arc::clone(&brain_stop),\n                Arc::clone(&reaper),\n                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n                // broker's live control-plane truth in-process; None on a bind-failed\n                // daemon degrades the gate to readiness-only.\n                broker_handle.clone(),\n            );\n            if let Some(broker) = &broker_handle {\n                broker.set_brain_restart(restart);\n            }\n        }\n\n        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness\n        // is the socket ping, never this file, so a stale value is harmless.\n        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n\n        // The seed-control channel is the foreground loop. Binding it is what\n        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n        // to start fails this bind and exits, leaving the winner serving. It\n        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);\n\n        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n        // does not respawn the brain we are about to kill) THEN reap the brain\n        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n        // spt.exe. The daemon is not in the reap container, so this never\n        // self-terminates: it returns cleanly below.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);\n        reaper.reap();\n        serve\n    }\n\n    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n    /// broker process spawns + supervises this; it connects to the broker over\n    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n…\n/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical\n/// socket names — but drops the std handles so it outlives the launching CLI\n/// call. The child is independent (not killed when the parent exits): on unix it\n/// is reparented to init when the launcher exits; on Windows a child is not in\n/// the parent's job by default.\n///\n/// **Windows handle-inheritance guard (KNOWN-HAZARDS 5.6).** A caller may\n/// capture this process's stdout/stderr through a pipe (`Command::output()`, a\n/// harness hook reading the command's output). On Windows `CreateProcess` runs\n/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n/// pipe's write end and hold it open forever — the capturing reader never sees\n/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n/// (the first guard shipped here) still leaked a grandparent capture's pipe\n/// sitting in our handle table as a stray. So detached long-lived children are\n/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n/// `CLOEXEC`) and keeps the plain `Command` path.\n///\n/// **Elevation guard (KNOWN-HAZARDS 5.7).** The daemon must ALWAYS run\n/// unelevated in the invoking user's universe, whichever command spawns it:\n/// an elevated spawner (Windows UAC, `sudo`) de-elevates the child here —\n/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n/// no handle inheritance by construction, so 5.6 holds on that path too),\n/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n/// torn one.\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\nfn spawn_detached() -> io::Result<()> {\n    let exe = std::env::current_exe()?;\n    #[cfg(windows)]\n    {\n        let exe = exe.to_string_lossy();\n        // `--detached` (hidden) tells the spawned broker it was launched by the\n        // auto-start ladder → it FreeConsole()s its own console at startup\n        // (REQ-HAZARD-WMI-DAEMON-WINDOW). The WMI/schtasks rungs run the broker via\n        // a `cmd /c … start /b spt daemon run` wrapper; @15fdf58 detached the cmd\n        // WRAPPER, but the inner broker (a console-subsystem exe) still ALLOCATES its\n        // own conhost = a window. The flag rides every rung (spawn_deelevated +\n        // launch_daemon_job_neutral); a human-typed `spt daemon run` omits it and\n        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n        let args = [\n            \"daemon\".to_string(),\n            \"run\".to_string(),\n            \"--detached\".to_string(),\n        ];\n        match crate::deelevate::spawn_deelevated(&exe, &args) {\n            Ok(Some(_pid)) => return Ok(()), // de-elevated child running\n            Ok(None) => {\n                if crate::deelevate::is_elevated() {\n                    // No unelevated identity exists (UAC-off admin desktop,\n                    // SYSTEM): the whole universe is uniformly elevated, so\n                    // an elevated daemon is consistent there. Loud anyway.\n                    eprintln!(\n                        \"ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \\\n                         the elevated token\"\n                    );\n                }\n            }\n            // A de-elevation TARGET existed but the spawn failed: an\n            // elevated daemon here is a known-torn state (its pipes deny\n            // the unelevated clients that target exists for). ABORT — do\n            // not start a daemon that cannot serve (user-ratified\n            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,\n            // don't limp).\n            Err(e) => {\n                return Err(io::Error::other(format!(\n                    \"DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \\\n                     unelevated clients); run spt from an unelevated shell\"\n                )));\n            }\n        }\n        launch_daemon_job_neutral(&exe, &args)?;\n        Ok(())\n    }\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        let mut cmd = Command::new(exe);\n        cmd.args([\"daemon\", \"run\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::null())\n            .stderr(Stdio::null());\n        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n            // Drop the child to the elected default user (else the sudo\n            // invoker — M8 decision 8): std clears supplementary groups and\n            // orders setgid-before-setuid when uid is set.\n            // [impl->REQ-INSTALL-6]\n            invoker.stamp_env(&mut cmd);\n            cmd.uid(invoker.uid).gid(invoker.gid);\n        }\n        // setsid: detach into a NEW SESSION, not just a new process group —\n        // a daemon auto-started inside an ssh/non-interactive session\n        // otherwise dies with it (sshd HUPs the whole session on\n        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n        // the freshly-spawned daemon vanished the moment the ssh command\n        // returned). `process_group(0)` is NOT enough — the new group\n        // still belongs to the dying session. Safety: this closure only\n        // calls the async-signal-safe setsid(2).\n        unsafe {\n            cmd.pre_exec(|| {\n                if libc::setsid() == -1 {\n                    return Err(std::io::Error::last_os_error());\n                }\n                Ok(())\n            });\n        }\n        cmd.spawn()?;\n        Ok(())\n    }\n}\n\n/// CREATE_BREAKAWAY_FROM_JOB: escape the launching terminal's Job Object.\n/// Windows Terminal / VS Code put the launched shell AND every descendant in a"}],"details":{"displayContent":{"text":"    pub fn run() -> io::Result<()> {\n…\n        if let Some(broker) = &broker_handle {\n…\n            thread::spawn(move || {\n                let _ = crate::tunnelhub::serve_tunnel_control(\n…\n                );\n            });\n        }\n\n        // Shell wake-watchers (M5-D4b) — the offline half of the shell\n        // online/offline exclusivity — now run in the BRAIN child (restoration\n        // D2-2): shellwake is brain-owned logic (ADR-0018 Q5 exception), a pure\n        // disk-reconciler whose boot sweep re-reconciles the watcher children on\n        // every brain start. The broker no longer hosts it; see\n        // [`crate::brainproc::run_brain`].\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n\n        // Brain process supervisor (ADR-0018 Q2/Q3 — broker/brain process\n        // restoration): the broker is the always-up per-machine anchor; it spawns\n        // `spt daemon brain` as a supervised child and respawns it on exit. D3-3\n        // makes an update a *planned* restart on this same path: the supervisor's\n        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n        // onto the new binary with `start-reason=update`. The stop flag is held\n        // for symmetry, never raised (the daemon runs until killed). The brain\n        // child inherits this process's already-unelevated token.\n        // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap\n        // container the supervisor enrolls every (re)spawned brain into. Created\n        // here so it outlives the supervisor and is reachable from the\n        // graceful-stop path below. The brain subtree (the brain + its shellwake\n        // watchers + its detached Psyches) reaps as one unit; the daemon itself is\n        // never in the container.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        let reaper = crate::reap::BrainReaper::new();\n        // The supervisor stop flag — hoisted out of the wiring block (was held\n        // \"for symmetry, never raised\") because the graceful-stop path NOW raises\n        // it, so the supervisor does not respawn the brain we are about to reap.\n        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        {\n            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(\n                Arc::clone(&brain_stop),\n                Arc::clone(&reaper),\n                // The DRAINED promotion gate (REQ-UPDATE-PROMOTE-DRAINED) reads this\n                // broker's live control-plane truth in-process; None on a bind-failed\n                // daemon degrades the gate to readiness-only.\n                broker_handle.clone(),\n            );\n            if let Some(broker) = &broker_handle {\n                broker.set_brain_restart(restart);\n            }\n        }\n\n        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness\n        // is the socket ping, never this file, so a stale value is harmless.\n        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n\n        // The seed-control channel is the foreground loop. Binding it is what\n        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n        // to start fails this bind and exits, leaving the winner serving. It\n        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);\n\n        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n        // does not respawn the brain we are about to kill) THEN reap the brain\n        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /\n        // spt.exe. The daemon is not in the reap container, so this never\n        // self-terminates: it returns cleanly below.\n        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);\n        reaper.reap();\n        serve\n    }\n\n    /// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The\n    /// broker process spawns + supervises this; it connects to the broker over\n    /// the existing socket IPC, signals ready, and idles. Not a user-facing\n    /// entry — `spt daemon run` (the broker) launches it. The broker stamps\n    /// `{generation, start-reason}` onto the argv (ADR-0018 V2); `start_reason`\n…\n/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical\n/// socket names — but drops the std handles so it outlives the launching CLI\n/// call. The child is independent (not killed when the parent exits): on unix it\n/// is reparented to init when the launcher exits; on Windows a child is not in\n/// the parent's job by default.\n///\n/// **Windows handle-inheritance guard (KNOWN-HAZARDS 5.6).** A caller may\n/// capture this process's stdout/stderr through a pipe (`Command::output()`, a\n/// harness hook reading the command's output). On Windows `CreateProcess` runs\n/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n/// pipe's write end and hold it open forever — the capturing reader never sees\n/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n/// (the first guard shipped here) still leaked a grandparent capture's pipe\n/// sitting in our handle table as a stray. So detached long-lived children are\n/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n/// `CLOEXEC`) and keeps the plain `Command` path.\n///\n/// **Elevation guard (KNOWN-HAZARDS 5.7).** The daemon must ALWAYS run\n/// unelevated in the invoking user's universe, whichever command spawns it:\n/// an elevated spawner (Windows UAC, `sudo`) de-elevates the child here —\n/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n/// no handle inheritance by construction, so 5.6 holds on that path too),\n/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n/// torn one.\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\nfn spawn_detached() -> io::Result<()> {\n    let exe = std::env::current_exe()?;\n    #[cfg(windows)]\n    {\n        let exe = exe.to_string_lossy();\n        // `--detached` (hidden) tells the spawned broker it was launched by the\n        // auto-start ladder → it FreeConsole()s its own console at startup\n        // (REQ-HAZARD-WMI-DAEMON-WINDOW). The WMI/schtasks rungs run the broker via\n        // a `cmd /c … start /b spt daemon run` wrapper; @15fdf58 detached the cmd\n        // WRAPPER, but the inner broker (a console-subsystem exe) still ALLOCATES its\n        // own conhost = a window. The flag rides every rung (spawn_deelevated +\n        // launch_daemon_job_neutral); a human-typed `spt daemon run` omits it and\n        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n        let args = [\n            \"daemon\".to_string(),\n            \"run\".to_string(),\n            \"--detached\".to_string(),\n        ];\n        match crate::deelevate::spawn_deelevated(&exe, &args) {\n            Ok(Some(_pid)) => return Ok(()), // de-elevated child running\n            Ok(None) => {\n                if crate::deelevate::is_elevated() {\n                    // No unelevated identity exists (UAC-off admin desktop,\n                    // SYSTEM): the whole universe is uniformly elevated, so\n                    // an elevated daemon is consistent there. Loud anyway.\n                    eprintln!(\n                        \"ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \\\n                         the elevated token\"\n                    );\n                }\n            }\n            // A de-elevation TARGET existed but the spawn failed: an\n            // elevated daemon here is a known-torn state (its pipes deny\n            // the unelevated clients that target exists for). ABORT — do\n            // not start a daemon that cannot serve (user-ratified\n            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,\n            // don't limp).\n            Err(e) => {\n                return Err(io::Error::other(format!(\n                    \"DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \\\n                     unelevated clients); run spt from an unelevated shell\"\n                )));\n            }\n        }\n        launch_daemon_job_neutral(&exe, &args)?;\n        Ok(())\n    }\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n        let mut cmd = Command::new(exe);\n        cmd.args([\"daemon\", \"run\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::null())\n            .stderr(Stdio::null());\n        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n            // Drop the child to the elected default user (else the sudo\n            // invoker — M8 decision 8): std clears supplementary groups and\n            // orders setgid-before-setuid when uid is set.\n            // [impl->REQ-INSTALL-6]\n            invoker.stamp_env(&mut cmd);\n            cmd.uid(invoker.uid).gid(invoker.gid);\n        }\n        // setsid: detach into a NEW SESSION, not just a new process group —\n        // a daemon auto-started inside an ssh/non-interactive session\n        // otherwise dies with it (sshd HUPs the whole session on\n        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n        // the freshly-spawned daemon vanished the moment the ssh command\n        // returned). `process_group(0)` is NOT enough — the new group\n        // still belongs to the dying session. Safety: this closure only\n        // calls the async-signal-safe setsid(2).\n        unsafe {\n            cmd.pre_exec(|| {\n                if libc::setsid() == -1 {\n                    return Err(std::io::Error::last_os_error());\n                }\n                Ok(())\n            });\n        }\n        cmd.spawn()?;\n        Ok(())\n    }\n}\n\n/// CREATE_BREAKAWAY_FROM_JOB: escape the launching terminal's Job Object.\n/// Windows Terminal / VS Code put the launched shell AND every descendant in a","startLine":46,"lineNumbers":[46,null,292,null,295,296,null,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\daemon.rs"}}},"isError":false,"timestamp":1784195850466}}
{"type":"message","id":"c11755de","parentId":"4d9a3bee","timestamp":"2026-07-16T09:57:30.467Z","message":{"role":"toolResult","toolCallId":"call_R2IVDmEr83xCSfb6D1BYmuv6|fc_0698befac979932d016a58ab089de8819a9d4f8e02817b8950","toolName":"read","content":[{"type":"text","text":"pub fn supervise_brain(\n…\n) {\n…\n    while !stop.load(Ordering::Relaxed) {\n…\n        match spawn_child(generation, reason, binary.as_deref()) {\n            Ok(mut child) => {\n…\n                if is_trial {\n                    match run_trial(&mut child, generation, window, env, stop) {\n…\n                        TrialStep::PreReadyExit => {\n                            pre_ready_exits += 1;\n                            eprintln!(\"BRAIN_TRIAL_EXIT: candidate exited pre-ready ({pre_ready_exits}/{TRIAL_PREREADY_BUDGET})\");\n                            if pre_ready_exits >= TRIAL_PREREADY_BUDGET {\n                                rollback(env, &record);\n                                pre_ready_exits = 0;\n                                // The rollback binary is known-good — respawn it\n                                // promptly, no crash-penalty backoff (mirror the\n                                // WindowElapsedAlive path); only the under-budget\n                                // candidate retries keep the doubling backoff.\n                                backoff = base;\n                            } else {\n                                backoff = next_backoff(backoff, started.elapsed(), base);\n                            }\n                            generation += 1;\n                            reason = StartReason::Crash;\n                            sleep_backoff(backoff, stop);\n                            continue;\n                        }\n                        TrialStep::WindowElapsedAlive => {\n                            // A11: KILL the alive-but-never-ready candidate BEFORE\n                            // spawning the rollback binary — two live brains would\n                            // break every single-brain seed/socket/IPC assumption.\n                            eprintln!(\"BRAIN_TRIAL_TIMEOUT: candidate alive but never ready — killing before rollback\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            rollback(env, &record);\n                            pre_ready_exits = 0;\n                            generation += 1;\n                            // The rollback binary is known-good — respawn promptly,\n                            // no crash-penalty backoff.\n                            reason = StartReason::Crash;\n                            backoff = base;\n                            continue;\n                        }\n                    }\n                }\n                if supervise {\n                    loop {\n                        if stop.load(Ordering::Relaxed) {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        // A planned brain-restart request (apply swapped the\n                        // binary, ADR-0018 D3-3): drop this brain so the respawn\n                        // lands on the new executable. A deliberate kill, NOT a\n                        // crash — stamp it `update`.\n                        if restart.take() {\n                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            planned = true;\n                            break;\n                        }\n                        match child.try_wait() {\n                            Ok(Some(status)) => {\n                                eprintln!(\"BRAIN_EXIT: brain child exited ({status}) — respawning\");\n                                break;\n                            }\n                            Ok(None) => thread::sleep(TICK),\n                            Err(e) => {\n                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            Err(e) => eprintln!(\"BRAIN_SPAWN_FAIL: {e}\"),\n        }\n        if stop.load(Ordering::Relaxed) {\n            return;\n        }\n        // Next spawn: advance the generation (every start, KH-2.4). A planned\n        // update respawns promptly with `start-reason=update` (it is not a crash,\n        // so the crash backoff does not apply); a crash recovery keeps the capped\n        // doubling backoff and stamps `crash`.\n        generation += 1;\n        if planned {\n            reason = StartReason::Update;\n            backoff = base;\n            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n            continue;\n        }\n        reason = StartReason::Crash;\n        backoff = next_backoff(backoff, started.elapsed(), base);\n        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n        sleep_backoff(backoff, stop);\n    }\n}\n\n/// Effect the rollback for an exhausted trial (D6-2): from the candidate's\n/// `AppliedPending` record, write `RolledBack{quarantine=N, running, rollback_binary}`\n/// and fire the loud notif (via `env`). `running` is the candidate's\n/// `prior_version` (the version actually running before the swap, captured by\n/// `apply`) — NOT `N-1`, which is wrong on a skip-version update (applied v5 →\n/// candidate v8: the `.old-8` aside holds v5 bytes, so `running` is v5, not v7).\n/// It degrades to `N-1` only for a pre-D6-2b record that lacks `prior_version`.\n/// A record that is not `AppliedPending` (already rolled back, or raced away) is\n/// a no-op — never a double rollback.\n///\n/// **Rollback is SELECTION, not an update (open-call-4).** The recovery never\n/// routes through the update engine / `apply` / `verify_metadata`: it just makes\n/// the supervisor *spawn the `.old-N` binary*. So the anti-downgrade monotonic\n/// version floor (`REQ-HAZARD-UPDATE-ROLLBACK` — *attack* protection that refuses\n/// a version *downgrade*) **never sees** this recovery, and the two concerns stay\n/// cleanly separate. Do NOT \"fix\" `verify_metadata` to permit recovery\n/// downgrades — selection-not-apply is the mechanism, by design.\nfn rollback(env: &dyn TrialEnv, record: &Option<AppliedRecord>) {\n    if let Some(AppliedRecord::AppliedPending {\n        version,\n        rollback_binary,\n        prior_version,\n        ..\n    }) = record\n    {\n        let running = prior_version.unwrap_or(version.saturating_sub(1));\n        eprintln!(\"BRAIN_ROLLBACK: update v{version} failed readiness — rolling back to v{running} ({rollback_binary}), quarantining v{version}\");\n        env.record_rolled_back(*version, running, rollback_binary);\n    }\n}\n\n/// Promotion bytes-gate verdict (KH 6.11, `REQ-HAZARD-BRAIN-RESPAWN-PATH`).\nenum BytesGate {\n    /// Candidate is running the staged bytes — promote.\n    Match,\n    /// Candidate is ready but running the WRONG bytes — fail the trial, roll back.\n    Mismatch,\n    /// Either hash absent (pre-metadata release / missing breadcrumb) — degrade\n    /// to readiness-only promotion (N-1-safe).\n    Unverified,\n}\n…\n/// broker process ([`crate::daemon::Daemon::run`]) before its foreground\n/// seed-control loop. The brain child is supervised for the broker's lifetime.\n/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn spawn_brain_supervisor(\n    stop: Arc<AtomicBool>,\n    reaper: Arc<crate::reap::BrainReaper>,\n    broker: Option<Arc<crate::broker::Broker>>,\n) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n    let restart = Arc::new(BrainRestart::new());\n    let restart_sup = Arc::clone(&restart);\n    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n    // start, before any `apply` can rename it under us. Linux current_exe()\n    // (readlink /proc/self/exe) is inode-tracking and would later follow the\n    // apply-rename (spt → spt.old-N) to the OLD bytes; capturing at t=0 pins the\n    // canonical path string, matching Windows' path-at-start semantics. The one\n    // true race — an apply landing in the microseconds between broker exec and\n    // this line — is vanishingly rare and backstopped by the promotion\n    // bytes-gate (a wrong-bytes trial rolls back loudly, never promotes).\n    let canonical_exe = std::env::current_exe().ok();\n    let handle = thread::spawn(move || {\n        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n        // readiness-only (no old-gen control plane to drain).\n        let env = ProductionTrialEnv { broker };\n        supervise_brain(\n            &stop,\n            &restart_sup,\n            SUPERVISE_BACKOFF_BASE,\n            &env,\n            SUPERVISE_HEALTHY_RUN,\n            |generation, reason, binary| {\n                let child =\n                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;\n                // Enroll the (re)spawned brain into the reap container so a\n                // `daemon stop` tears down the brain + its Psyches as one unit\n                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n                reaper.enroll(&child);\n                Ok(child)\n            },\n        );\n    });\n    (handle, restart)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::sync::atomic::AtomicU32;\n\n…\n}"}],"details":{"displayContent":{"text":"pub fn supervise_brain(\n…\n) {\n…\n    while !stop.load(Ordering::Relaxed) {\n…\n        match spawn_child(generation, reason, binary.as_deref()) {\n            Ok(mut child) => {\n…\n                if is_trial {\n                    match run_trial(&mut child, generation, window, env, stop) {\n…\n                        TrialStep::PreReadyExit => {\n                            pre_ready_exits += 1;\n                            eprintln!(\"BRAIN_TRIAL_EXIT: candidate exited pre-ready ({pre_ready_exits}/{TRIAL_PREREADY_BUDGET})\");\n                            if pre_ready_exits >= TRIAL_PREREADY_BUDGET {\n                                rollback(env, &record);\n                                pre_ready_exits = 0;\n                                // The rollback binary is known-good — respawn it\n                                // promptly, no crash-penalty backoff (mirror the\n                                // WindowElapsedAlive path); only the under-budget\n                                // candidate retries keep the doubling backoff.\n                                backoff = base;\n                            } else {\n                                backoff = next_backoff(backoff, started.elapsed(), base);\n                            }\n                            generation += 1;\n                            reason = StartReason::Crash;\n                            sleep_backoff(backoff, stop);\n                            continue;\n                        }\n                        TrialStep::WindowElapsedAlive => {\n                            // A11: KILL the alive-but-never-ready candidate BEFORE\n                            // spawning the rollback binary — two live brains would\n                            // break every single-brain seed/socket/IPC assumption.\n                            eprintln!(\"BRAIN_TRIAL_TIMEOUT: candidate alive but never ready — killing before rollback\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            rollback(env, &record);\n                            pre_ready_exits = 0;\n                            generation += 1;\n                            // The rollback binary is known-good — respawn promptly,\n                            // no crash-penalty backoff.\n                            reason = StartReason::Crash;\n                            backoff = base;\n                            continue;\n                        }\n                    }\n                }\n                if supervise {\n                    loop {\n                        if stop.load(Ordering::Relaxed) {\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            return;\n                        }\n                        // A planned brain-restart request (apply swapped the\n                        // binary, ADR-0018 D3-3): drop this brain so the respawn\n                        // lands on the new executable. A deliberate kill, NOT a\n                        // crash — stamp it `update`.\n                        if restart.take() {\n                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n                            let _ = child.kill();\n                            let _ = child.wait();\n                            planned = true;\n                            break;\n                        }\n                        match child.try_wait() {\n                            Ok(Some(status)) => {\n                                eprintln!(\"BRAIN_EXIT: brain child exited ({status}) — respawning\");\n                                break;\n                            }\n                            Ok(None) => thread::sleep(TICK),\n                            Err(e) => {\n                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            Err(e) => eprintln!(\"BRAIN_SPAWN_FAIL: {e}\"),\n        }\n        if stop.load(Ordering::Relaxed) {\n            return;\n        }\n        // Next spawn: advance the generation (every start, KH-2.4). A planned\n        // update respawns promptly with `start-reason=update` (it is not a crash,\n        // so the crash backoff does not apply); a crash recovery keeps the capped\n        // doubling backoff and stamps `crash`.\n        generation += 1;\n        if planned {\n            reason = StartReason::Update;\n            backoff = base;\n            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n            continue;\n        }\n        reason = StartReason::Crash;\n        backoff = next_backoff(backoff, started.elapsed(), base);\n        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n        sleep_backoff(backoff, stop);\n    }\n}\n\n/// Effect the rollback for an exhausted trial (D6-2): from the candidate's\n/// `AppliedPending` record, write `RolledBack{quarantine=N, running, rollback_binary}`\n/// and fire the loud notif (via `env`). `running` is the candidate's\n/// `prior_version` (the version actually running before the swap, captured by\n/// `apply`) — NOT `N-1`, which is wrong on a skip-version update (applied v5 →\n/// candidate v8: the `.old-8` aside holds v5 bytes, so `running` is v5, not v7).\n/// It degrades to `N-1` only for a pre-D6-2b record that lacks `prior_version`.\n/// A record that is not `AppliedPending` (already rolled back, or raced away) is\n/// a no-op — never a double rollback.\n///\n/// **Rollback is SELECTION, not an update (open-call-4).** The recovery never\n/// routes through the update engine / `apply` / `verify_metadata`: it just makes\n/// the supervisor *spawn the `.old-N` binary*. So the anti-downgrade monotonic\n/// version floor (`REQ-HAZARD-UPDATE-ROLLBACK` — *attack* protection that refuses\n/// a version *downgrade*) **never sees** this recovery, and the two concerns stay\n/// cleanly separate. Do NOT \"fix\" `verify_metadata` to permit recovery\n/// downgrades — selection-not-apply is the mechanism, by design.\nfn rollback(env: &dyn TrialEnv, record: &Option<AppliedRecord>) {\n    if let Some(AppliedRecord::AppliedPending {\n        version,\n        rollback_binary,\n        prior_version,\n        ..\n    }) = record\n    {\n        let running = prior_version.unwrap_or(version.saturating_sub(1));\n        eprintln!(\"BRAIN_ROLLBACK: update v{version} failed readiness — rolling back to v{running} ({rollback_binary}), quarantining v{version}\");\n        env.record_rolled_back(*version, running, rollback_binary);\n    }\n}\n\n/// Promotion bytes-gate verdict (KH 6.11, `REQ-HAZARD-BRAIN-RESPAWN-PATH`).\nenum BytesGate {\n    /// Candidate is running the staged bytes — promote.\n    Match,\n    /// Candidate is ready but running the WRONG bytes — fail the trial, roll back.\n    Mismatch,\n    /// Either hash absent (pre-metadata release / missing breadcrumb) — degrade\n    /// to readiness-only promotion (N-1-safe).\n    Unverified,\n}\n…\n/// broker process ([`crate::daemon::Daemon::run`]) before its foreground\n/// seed-control loop. The brain child is supervised for the broker's lifetime.\n/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn spawn_brain_supervisor(\n    stop: Arc<AtomicBool>,\n    reaper: Arc<crate::reap::BrainReaper>,\n    broker: Option<Arc<crate::broker::Broker>>,\n) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {\n    let restart = Arc::new(BrainRestart::new());\n    let restart_sup = Arc::clone(&restart);\n    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n    // start, before any `apply` can rename it under us. Linux current_exe()\n    // (readlink /proc/self/exe) is inode-tracking and would later follow the\n    // apply-rename (spt → spt.old-N) to the OLD bytes; capturing at t=0 pins the\n    // canonical path string, matching Windows' path-at-start semantics. The one\n    // true race — an apply landing in the microseconds between broker exec and\n    // this line — is vanishingly rare and backstopped by the promotion\n    // bytes-gate (a wrong-bytes trial rolls back loudly, never promotes).\n    let canonical_exe = std::env::current_exe().ok();\n    let handle = thread::spawn(move || {\n        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n        // readiness-only (no old-gen control plane to drain).\n        let env = ProductionTrialEnv { broker };\n        supervise_brain(\n            &stop,\n            &restart_sup,\n            SUPERVISE_BACKOFF_BASE,\n            &env,\n            SUPERVISE_HEALTHY_RUN,\n            |generation, reason, binary| {\n                let child =\n                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;\n                // Enroll the (re)spawned brain into the reap container so a\n                // `daemon stop` tears down the brain + its Psyches as one unit\n                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]\n                reaper.enroll(&child);\n                Ok(child)\n            },\n        );\n    });\n    (handle, restart)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::sync::atomic::AtomicU32;\n\n…\n}","startLine":749,"lineNumbers":[749,null,756,null,770,null,797,798,null,803,804,null,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,null,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,2115]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"}}},"isError":false,"timestamp":1784195850466}}
{"type":"message","id":"d7443596","parentId":"c11755de","timestamp":"2026-07-16T09:57:30.497Z","message":{"role":"toolResult","toolCallId":"call_guFv0r25aSiiM8F2AjJzgCRV|fc_0698befac979932d016a58ab089e88819ab3ceb99594ad8138","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…\n}\n…\nimpl Drop for RawGuard {\n    fn drop(&mut self) {\n…\n        if let Some(prior) = self.prior_out_mode {\n            restore_out_mode(prior);\n        }\n        if self.raw {\n            let _ = crossterm::terminal::disable_raw_mode();\n        }\n    }\n}\n\n/// Resolve a live endpoint id to its broker session id via the broker's hosted\n/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no\n/// live session under that endpoint.\nfn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {\n    let reply = brain.sessions()?;\n    Ok(reply\n        .sessions\n        .into_iter()\n        .find(|s| s.endpoint == endpoint_id)\n        .map(|s| s.session_id))\n}\n\n/// A thin, reusable broker connection for polling \"is there a live session for\n/// this endpoint?\" — the W4 attach-on-session-exists gate\n/// (REQ-ENDPOINT-UNBOUND-ATTACH). One broker connect, then [`has_session`] is\n/// queried across the bounded await loop (so the await never re-dials per\n/// iteration). Keys on the broker sessions map (ADR-0025 W3a) — independent of\n/// perch status, so an UNBOUND skeleton's live session reports attachable.\n///\n/// [`has_session`]: SessionProbe::has_session\npub(crate) struct SessionProbe {\n    brain: Brain,\n}\n\nimpl SessionProbe {\n    /// Cold-connect to the broker once; the connection is reused across the\n    /// await poll. `Err` ⇒ the broker is unreachable (the caller fails loud).\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn connect() -> Result<Self, String> {\n        let brain = Brain::cold_start_pump(\n            &spt_daemon::endpoint::broker_socket_name(),\n            now_ms(),\n            Duration::from_secs(10),\n        )\n        .map_err(|e| format!(\"broker connect: {e}\"))?;\n        Ok(Self { brain })\n    }\n\n    /// `true` when a live broker session exists for `endpoint_id` (ANY perch\n    /// status — an UNBOUND session counts). A query error reads as \"not yet\".\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn has_session(&mut self, endpoint_id: &str) -> bool {\n        resolve_session(&mut self.brain, endpoint_id)\n            .ok()\n            .flatten()\n            .is_some()\n    }\n}\n\n/// The node currently driving `endpoint_id`, if any — read off the perch's\n/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side\n/// busy-refuse guidance. `None` ⇒ free (or no perch).\nfn current_driver(endpoint_id: &str) -> Option<String> {\n    let perch = spt_store::perch::resolve_perch_path(\n        endpoint_id,\n        spt_store::perch::ParentHint::Infer,\n    );\n    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)\n}\n\n/// What one stdin chunk yielded after the detach state machine: the bytes to\n/// forward to the session, and whether the detach keybind fired.\nstruct DetachParse {\n    forward: Vec<u8>,\n    detach: bool,\n}\n\n/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds\n/// the \"saw the prefix\" state across chunk boundaries (a prefix can end one read\n/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →\n/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other\n/// byte passes through. Pure so it unit-tests without a terminal. On detach the\n/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the\n/// viewport is ending).\nfn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n    let mut forward = Vec::with_capacity(input.len());\n    for &b in input {\n        if *armed {\n            *armed = false;\n            match b {\n                DETACH_KEY => return DetachParse { forward, detach: true },\n                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n                other => {\n                    forward.push(DETACH_PREFIX);\n                    forward.push(other);\n                }\n            }\n        } else if b == DETACH_PREFIX {\n            *armed = true;\n        } else {\n            forward.push(b);\n        }\n    }\n…\n}\n…\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // message, never the historical infinite blank.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n                 `spt endpoint run` to restart it]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::NoLiveSession) => {\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n            // no live session for the endpoint on the node we reached, or its gate\n            // denied us. On the cross-node leg this is the D6 stale-row case.\n            match &remote_node {\n                Some(node) => {\n                    let _ = writeln!(\n                        stdout,\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n                         have ended, or the registry row is stale]\"\n                    );\n                }\n                None => {\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n                }\n            }\n            Ok(())\n        }\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\n    }\n}\n\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n/// observable situation + next action; every OTHER error passes through\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n/// the mapping is unit-tested.\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\n    if e.contains(\"brain IPC read deadline\") {\n        format!(\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\n        )\n    } else {\n        e\n    }\n}\n\n/// An established attach viewport: the broker conn, the open+subscribed attach\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n/// viewport). [impl->REQ-RC-RECONNECT]\nstruct Established {\n    brain: Brain,\n    ops: OpMinter,\n    stream_id: u64,\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\n    /// + the honest-failure copy); `None` ⇒ local.\n    remote_node: Option<String>,\n}\n\n/// Why [`establish_attach`] failed.\nenum EstablishFail {\n    /// No attachable target (no live session anywhere / ambiguous / node\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n    /// the reconnect loop retries it inside the window instead (the node may\n    /// still be coming back).\n    NoTarget(String),\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n…\n                },\n            };\n…\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n…\n}"}],"details":{"displayContent":{"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…\n}\n…\nimpl Drop for RawGuard {\n    fn drop(&mut self) {\n…\n        if let Some(prior) = self.prior_out_mode {\n            restore_out_mode(prior);\n        }\n        if self.raw {\n            let _ = crossterm::terminal::disable_raw_mode();\n        }\n    }\n}\n\n/// Resolve a live endpoint id to its broker session id via the broker's hosted\n/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no\n/// live session under that endpoint.\nfn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {\n    let reply = brain.sessions()?;\n    Ok(reply\n        .sessions\n        .into_iter()\n        .find(|s| s.endpoint == endpoint_id)\n        .map(|s| s.session_id))\n}\n\n/// A thin, reusable broker connection for polling \"is there a live session for\n/// this endpoint?\" — the W4 attach-on-session-exists gate\n/// (REQ-ENDPOINT-UNBOUND-ATTACH). One broker connect, then [`has_session`] is\n/// queried across the bounded await loop (so the await never re-dials per\n/// iteration). Keys on the broker sessions map (ADR-0025 W3a) — independent of\n/// perch status, so an UNBOUND skeleton's live session reports attachable.\n///\n/// [`has_session`]: SessionProbe::has_session\npub(crate) struct SessionProbe {\n    brain: Brain,\n}\n\nimpl SessionProbe {\n    /// Cold-connect to the broker once; the connection is reused across the\n    /// await poll. `Err` ⇒ the broker is unreachable (the caller fails loud).\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn connect() -> Result<Self, String> {\n        let brain = Brain::cold_start_pump(\n            &spt_daemon::endpoint::broker_socket_name(),\n            now_ms(),\n            Duration::from_secs(10),\n        )\n        .map_err(|e| format!(\"broker connect: {e}\"))?;\n        Ok(Self { brain })\n    }\n\n    /// `true` when a live broker session exists for `endpoint_id` (ANY perch\n    /// status — an UNBOUND session counts). A query error reads as \"not yet\".\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    pub(crate) fn has_session(&mut self, endpoint_id: &str) -> bool {\n        resolve_session(&mut self.brain, endpoint_id)\n            .ok()\n            .flatten()\n            .is_some()\n    }\n}\n\n/// The node currently driving `endpoint_id`, if any — read off the perch's\n/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side\n/// busy-refuse guidance. `None` ⇒ free (or no perch).\nfn current_driver(endpoint_id: &str) -> Option<String> {\n    let perch = spt_store::perch::resolve_perch_path(\n        endpoint_id,\n        spt_store::perch::ParentHint::Infer,\n    );\n    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)\n}\n\n/// What one stdin chunk yielded after the detach state machine: the bytes to\n/// forward to the session, and whether the detach keybind fired.\nstruct DetachParse {\n    forward: Vec<u8>,\n    detach: bool,\n}\n\n/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds\n/// the \"saw the prefix\" state across chunk boundaries (a prefix can end one read\n/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →\n/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other\n/// byte passes through. Pure so it unit-tests without a terminal. On detach the\n/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the\n/// viewport is ending).\nfn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n    let mut forward = Vec::with_capacity(input.len());\n    for &b in input {\n        if *armed {\n            *armed = false;\n            match b {\n                DETACH_KEY => return DetachParse { forward, detach: true },\n                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n                other => {\n                    forward.push(DETACH_PREFIX);\n                    forward.push(other);\n                }\n            }\n        } else if b == DETACH_PREFIX {\n            *armed = true;\n        } else {\n            forward.push(b);\n        }\n    }\n…\n}\n…\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // message, never the historical infinite blank.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n                 `spt endpoint run` to restart it]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::NoLiveSession) => {\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n            // no live session for the endpoint on the node we reached, or its gate\n            // denied us. On the cross-node leg this is the D6 stale-row case.\n            match &remote_node {\n                Some(node) => {\n                    let _ = writeln!(\n                        stdout,\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n                         have ended, or the registry row is stale]\"\n                    );\n                }\n                None => {\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n                }\n            }\n            Ok(())\n        }\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\n    }\n}\n\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n/// observable situation + next action; every OTHER error passes through\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n/// the mapping is unit-tested.\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\n    if e.contains(\"brain IPC read deadline\") {\n        format!(\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\n        )\n    } else {\n        e\n    }\n}\n\n/// An established attach viewport: the broker conn, the open+subscribed attach\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n/// viewport). [impl->REQ-RC-RECONNECT]\nstruct Established {\n    brain: Brain,\n    ops: OpMinter,\n    stream_id: u64,\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\n    /// + the honest-failure copy); `None` ⇒ local.\n    remote_node: Option<String>,\n}\n\n/// Why [`establish_attach`] failed.\nenum EstablishFail {\n    /// No attachable target (no live session anywhere / ambiguous / node\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n    /// the reconnect loop retries it inside the window instead (the node may\n    /// still be coming back).\n    NoTarget(String),\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n…\n                },\n            };\n…\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,null,52,null,841,842,null,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,null,955,null,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,null,1484,1485,null,1487,1488,1489,null,1501]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784195850497}}
{"type":"message","id":"ebeda089","parentId":"d7443596","timestamp":"2026-07-16T09:57:30.505Z","message":{"role":"toolResult","toolCallId":"call_OnwsyNmETXk5I22mnT8NT2bT|fc_0698befac979932d016a58ab089e40819a8f97a2df3801716a","toolName":"read","content":[{"type":"text","text":"//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n//! onto a local session for an operator on another node.\n//!\n//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n//! [`AttachRecord::Request`]; this module is the target-side pump between that\n//! stream and the local broker-hosted session:\n//!\n//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n//!   journal (a replayed input record cannot double-type);\n//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n//!\n//! ## Exactly-once lives at the EDGES, not the wire sends\n//!\n//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n//! and *discards* everything interleaved — inside a full-duplex pump loop that\n//! eats the operator's `Input` records (both sides starve; found the hard way\n//! in `tests/attach.rs`). Idempotency holds without it because each direction\n//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n//! session-log `seq` (the render cursor). A post-restart re-serve may\n//! re-transmit output records on the wire — duplicates are dropped at the\n//! render cursor, the same discipline the local `OutputLog` replay uses\n//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n//!\n//! **Compute + files stay here**; the operator is a viewport (CONTEXT\n//! §Remote-control vs local operation). The access gate runs before anything\n//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\n//! [`access_check`] sees the **handshake-proven** origin from the broker's\n//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n//! your own running instance is ungated by default (ADR-0009 default-open);\n//! the D5d whitelist drops in at this same call.\n//!\n//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n//! A restarted target brain re-attaches the session from its output cursor,\n//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n//! a restarted operator brain resubscribes and dedups rendered chunks by\n//! `Output.seq`.\n\nuse std::io;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::ndjson;\n\nuse crate::access::{access_check, AccessDecision, InboundClass};\nuse crate::brain::{now_ms, Brain, BrokerEvent};\nuse crate::effect::{Minter, MintedOp};\nuse crate::msg::{decode_bytes, encode_bytes};\n\n/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n/// served viewport is a **driver** — attaching wakes the endpoint, the\n/// viewport ending detaches it (active → dormant; the transition echo is\n/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n/// [`mark_driven_by`]: a session served without a perch has no resting\n/// record to move.\n// [impl->REQ-INST-3]\nfn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n}\n\n/// Why a serve loop returned.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum AttachServeOutcome {\n    /// The access gate refused the requesting origin, OR a `Control` attach hit\n    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n    /// busy guidance client-side from the endpoint's `driven_by`.\n    Refused,\n    /// The session's child exited; the `Exit` record was sent and the stream\n    /// finished.\n    Exited(Option<i32>),\n    /// The operator closed its send side (detached); the viewport ended with\n    /// the session still running.\n    Detached,\n    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n    Displaced,\n}\n\n/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n/// arrives — gates wake (controller-only) and PTY input.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ServeRole {\n    /// The interactive controller (input + resize; wakes the endpoint).\n    Controller,\n    /// A read-only viewer (output only; wake-neutral).\n    Viewer,\n}\n\n/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\n/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\n/// hopelessly behind under a sustained flood will be re-evicted immediately after\n/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n/// it, the operator sees an intermittent LIVE burst each interval (forward\n/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\nconst RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n\n/// The CONTROLLER fell behind the live ring further than the ring holds — the\n/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n#[derive(Debug)]\npub struct ControllerIrrecoverablyBehind {\n    /// The frozen `delivered_through` floor the ring has already rolled past.\n    pub floor: u64,\n}\n…\npub fn read_controller_event_resuming(\n…\n) -> io::Result<BrokerEvent> {\n…\n    loop {\n        match brain.read_event() {\n…\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\n                swallow_resume_ack = false;\n                continue;\n            }\n            Ok(ev) => return Ok(ev),\n            Err(e) if is_output_gap(&e) => {\n                let floor = brain.controller_resume_floor();\n                if last_resume_floor == Some(floor) {\n                    // No progress across two resumes → the ring rolled past the\n                    // floor → the dropped frames are gone.\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\n    remote_session: Option<u64>,\n) -> io::Result<AttachServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\n    // session output (broker→serve) — including the controller's synchronous,\n    // inline, multi-frame ring replay (`become_controller`), which holds the\n    // broker's single-threaded per-conn handler for the whole replay. If the\n    // forwards rode `brain` too, that stuck handler could not read them →\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n    // two directions on independent single-threaded handlers cannot block each\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n    // a second conn is correct. Order is preserved: this one serve thread reads\n    // every Output in seq order on `brain` and forwards it in that same order on\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n    // the controller onto that async path would remove the inline-replay block at\n    // the source, but it touches the authoritative cursor-advancing path.)\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    let mut decoder = AttachDecoder::new();\n    let mut attached = false;\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\n    // race on the input gate).\n    let mut intent = AttachIntent::Control;\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\n    // controller input that races ahead of the reply is BUFFERED, not dropped\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\n    // we know we control the session (the busy-refuse case).\n    let mut role: Option<ServeRole> = None;\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n    // The attached session — captured from the Request so the terminal paths can\n    // explicitly release this worker's role (clearing driven_by / viewer_count\n    // promptly, not only on connection drop).\n    let mut attached_session: Option<u64> = None;\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n    // intermittent live bursts, never a CPU spin.\n    let mut last_resubscribe: Option<Instant> = None;\n\n    loop {\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\n        // role resolves no Output flows, so a plain read is correct then.\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n        let event = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\n                    Ok(ev) => ev,\n                    Err(e)\n                        if e.get_ref()\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\n                    {\n                        // Irrecoverable: the dropped frames rolled out of the ring.\n                        // SURFACE a marked truncation to the operator (never a\n                        // silent skip = B2 lie, never a spin), then end cleanly.\n                        // Graceful continuation is deferred\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\n                        let notice =\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n                        let line = ndjson::encode_line(&AttachRecord::Output {\n                            seq: brain.controller_resume_floor(),\n                            data_b64: encode_bytes(notice),\n                        });\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\n                            // the transport-proven origin. Viewing is gated\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\n                            // watching reveals full session contents).\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\n                                == AccessDecision::Deny\n                            {\n                                wire.net_stream_send(stream_id, &[], None, true)?;\n                                return Ok(AttachServeOutcome::Refused);\n                            }\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\n                            // dispatcher-resolved session id (the owning node is\n                            // authoritative for its own table); the LOCAL path uses\n                            // the wire session_id unchanged.\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\n                            let sid = remote_session.unwrap_or(session_id);\n                            intent = req_intent;\n                            attached_session = Some(sid);\n…\n                        }\n…\n                    }\n                }\n            }\n…\n            BrokerEvent::Subscribed { outcome, .. } => {\n…\n                match outcome {\n…\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n            BrokerEvent::Size { rows, cols, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            // This controller was kicked by a different operator's `--take`.\n            BrokerEvent::Displaced { by, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                // No dormant edge: the endpoint stays driven by the taker (whose\n                // own serve loop already fired the wake).\n                return Ok(AttachServeOutcome::Displaced);\n            }\n\n            // This VIEWER overflowed its broker subscription queue and was evicted\n            // (it forwards slower than the drain fans out under flood). SKIP TO\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\n            // spin. The next live Output carries a forward seq jump; the operator's\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n            BrokerEvent::ViewerEvicted { session_id: sid }\n                if attached && Some(sid) == attached_session =>\n            {\n                if let Some(prev) = last_resubscribe {\n                    let since = prev.elapsed();\n                    if since < RESUBSCRIBE_INTERVAL {\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\n                    }\n                }\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\n                last_resubscribe = Some(Instant::now());\n            }\n\n            // The operator closed its send side: the viewport is over.\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                wire.net_stream_send(stream_id, &[], None, true)?;\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n                // info.json, carrying the current driven_by through), THEN release\n                // the role over IPC so the broker's driven_by=None is the LAST\n                // write — otherwise a concurrent resting RMW restores the stale\n                // marker (the clear-race the broker-single-writer model averts).\n                if role == Some(ServeRole::Controller) {\n                    // The driver detached — the endpoint rests warm (controller-\n                    // only; a viewer leaving is wake-neutral).\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                if let Some(sid) = attached_session {\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\n                }\n                return Ok(AttachServeOutcome::Detached);\n            }\n\n            // ── Session → wire: output and exit. ────────────────────────────\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Output {\n                    seq,\n                    data_b64: encode_bytes(&bytes),\n                });\n                // Unjournaled by design (see module docs): `seq` rides inside\n                // the record, and the operator's render cursor is the dedup.\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            BrokerEvent::Exit { code, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Ok(AttachServeOutcome::Exited(code));\n            }\n\n            BrokerEvent::Error { message } => {\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Err(io::Error::other(message));\n            }\n            // Applied/NetSent acks and unrelated events: not ours to handle.\n            _ => {}\n        }\n    }\n}\n\n/// Operator-side: open an attach stream on a dialed connection and send the\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n/// durable id for the exactly-once stream open (safe to block for its ack —\n/// no attach traffic can be in flight before the stream exists); the Request\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\n/// and re-attaches idempotently).\n// [impl->REQ-INST-8]\npub fn request_attach(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n) -> io::Result<u64> {\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n}\n\n/// Like [`request_attach`], but for the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the remote operator cannot know the owning node's\n/// local session id, so it passes `endpoint_id = Some(endpoint)` (and a\n/// placeholder `session_id` of `0` the owning node ignores). The owning node\n/// resolves the endpoint to its OWN session id server-side\n/// ([`resolve_local_session`]) — authoritative for its own table. `None` here is\n/// exactly [`request_attach`]: the local path's wire bytes stay byte-identical.\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn request_attach_endpoint(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n    endpoint_id: Option<&str>,\n) -> io::Result<u64> {\n    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n    let line = ndjson::encode_line(&AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent,\n        endpoint_id: endpoint_id.map(str::to_string),\n    });\n    brain.net_stream_send(opened.stream_id, &line, None, false)?;\n    Ok(opened.stream_id)\n}\n\n/// Operator-side (CONTROLLER): send a resize record onto an attach stream —\n/// the controller's viewport size, forwarded into the target PTY (REQ-RCVIEW-1,\n/// controller-exclusive: the broker rejects a resize from a viewer's stream).\n/// Sent on attach (initial size) and on every window-change. Unjournaled (a\n/// stale resize is harmless — the next one corrects it).\n// [impl->REQ-RCVIEW-1]\npub fn send_attach_resize(\n    brain: &mut Brain,\n    stream_id: u64,\n    rows: u16,\n    cols: u16,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}\n\n/// Operator-side: send one input record (the keystrokes) onto an attach\n/// stream. `wire_op` is the operator-minted durable id the TARGET journals the\n/// PTY write under — exactly-once at the keys, however often the record\n/// crosses the wire. The send itself is unjournaled (see module docs).\n// [impl->REQ-INST-8]\npub fn send_attach_input(\n    brain: &mut Brain,\n    stream_id: u64,\n    bytes: &[u8],\n    wire_op: u64,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Input {\n        data_b64: encode_bytes(bytes),\n        op_id: wire_op,\n    });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}"}],"details":{"displayContent":{"text":"//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n//! onto a local session for an operator on another node.\n//!\n//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n//! [`AttachRecord::Request`]; this module is the target-side pump between that\n//! stream and the local broker-hosted session:\n//!\n//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n//!   journal (a replayed input record cannot double-type);\n//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n//!\n//! ## Exactly-once lives at the EDGES, not the wire sends\n//!\n//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n//! and *discards* everything interleaved — inside a full-duplex pump loop that\n//! eats the operator's `Input` records (both sides starve; found the hard way\n//! in `tests/attach.rs`). Idempotency holds without it because each direction\n//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n//! session-log `seq` (the render cursor). A post-restart re-serve may\n//! re-transmit output records on the wire — duplicates are dropped at the\n//! render cursor, the same discipline the local `OutputLog` replay uses\n//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n//!\n//! **Compute + files stay here**; the operator is a viewport (CONTEXT\n//! §Remote-control vs local operation). The access gate runs before anything\n//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\n//! [`access_check`] sees the **handshake-proven** origin from the broker's\n//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n//! your own running instance is ungated by default (ADR-0009 default-open);\n//! the D5d whitelist drops in at this same call.\n//!\n//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n//! A restarted target brain re-attaches the session from its output cursor,\n//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n//! a restarted operator brain resubscribes and dedups rendered chunks by\n//! `Output.seq`.\n\nuse std::io;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::ndjson;\n\nuse crate::access::{access_check, AccessDecision, InboundClass};\nuse crate::brain::{now_ms, Brain, BrokerEvent};\nuse crate::effect::{Minter, MintedOp};\nuse crate::msg::{decode_bytes, encode_bytes};\n\n/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n/// served viewport is a **driver** — attaching wakes the endpoint, the\n/// viewport ending detaches it (active → dormant; the transition echo is\n/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n/// [`mark_driven_by`]: a session served without a perch has no resting\n/// record to move.\n// [impl->REQ-INST-3]\nfn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n}\n\n/// Why a serve loop returned.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum AttachServeOutcome {\n    /// The access gate refused the requesting origin, OR a `Control` attach hit\n    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n    /// busy guidance client-side from the endpoint's `driven_by`.\n    Refused,\n    /// The session's child exited; the `Exit` record was sent and the stream\n    /// finished.\n    Exited(Option<i32>),\n    /// The operator closed its send side (detached); the viewport ended with\n    /// the session still running.\n    Detached,\n    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n    Displaced,\n}\n\n/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n/// arrives — gates wake (controller-only) and PTY input.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ServeRole {\n    /// The interactive controller (input + resize; wakes the endpoint).\n    Controller,\n    /// A read-only viewer (output only; wake-neutral).\n    Viewer,\n}\n\n/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\n/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\n/// hopelessly behind under a sustained flood will be re-evicted immediately after\n/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n/// it, the operator sees an intermittent LIVE burst each interval (forward\n/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\nconst RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n\n/// The CONTROLLER fell behind the live ring further than the ring holds — the\n/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n#[derive(Debug)]\npub struct ControllerIrrecoverablyBehind {\n    /// The frozen `delivered_through` floor the ring has already rolled past.\n    pub floor: u64,\n}\n…\npub fn read_controller_event_resuming(\n…\n) -> io::Result<BrokerEvent> {\n…\n    loop {\n        match brain.read_event() {\n…\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\n                swallow_resume_ack = false;\n                continue;\n            }\n            Ok(ev) => return Ok(ev),\n            Err(e) if is_output_gap(&e) => {\n                let floor = brain.controller_resume_floor();\n                if last_resume_floor == Some(floor) {\n                    // No progress across two resumes → the ring rolled past the\n                    // floor → the dropped frames are gone.\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\n    remote_session: Option<u64>,\n) -> io::Result<AttachServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\n    // session output (broker→serve) — including the controller's synchronous,\n    // inline, multi-frame ring replay (`become_controller`), which holds the\n    // broker's single-threaded per-conn handler for the whole replay. If the\n    // forwards rode `brain` too, that stuck handler could not read them →\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n    // two directions on independent single-threaded handlers cannot block each\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n    // a second conn is correct. Order is preserved: this one serve thread reads\n    // every Output in seq order on `brain` and forwards it in that same order on\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n    // the controller onto that async path would remove the inline-replay block at\n    // the source, but it touches the authoritative cursor-advancing path.)\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    let mut decoder = AttachDecoder::new();\n    let mut attached = false;\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\n    // race on the input gate).\n    let mut intent = AttachIntent::Control;\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\n    // controller input that races ahead of the reply is BUFFERED, not dropped\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\n    // we know we control the session (the busy-refuse case).\n    let mut role: Option<ServeRole> = None;\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n    // The attached session — captured from the Request so the terminal paths can\n    // explicitly release this worker's role (clearing driven_by / viewer_count\n    // promptly, not only on connection drop).\n    let mut attached_session: Option<u64> = None;\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n    // intermittent live bursts, never a CPU spin.\n    let mut last_resubscribe: Option<Instant> = None;\n\n    loop {\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\n        // role resolves no Output flows, so a plain read is correct then.\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n        let event = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\n                    Ok(ev) => ev,\n                    Err(e)\n                        if e.get_ref()\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\n                    {\n                        // Irrecoverable: the dropped frames rolled out of the ring.\n                        // SURFACE a marked truncation to the operator (never a\n                        // silent skip = B2 lie, never a spin), then end cleanly.\n                        // Graceful continuation is deferred\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\n                        let notice =\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n                        let line = ndjson::encode_line(&AttachRecord::Output {\n                            seq: brain.controller_resume_floor(),\n                            data_b64: encode_bytes(notice),\n                        });\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\n                            // the transport-proven origin. Viewing is gated\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\n                            // watching reveals full session contents).\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\n                                == AccessDecision::Deny\n                            {\n                                wire.net_stream_send(stream_id, &[], None, true)?;\n                                return Ok(AttachServeOutcome::Refused);\n                            }\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\n                            // dispatcher-resolved session id (the owning node is\n                            // authoritative for its own table); the LOCAL path uses\n                            // the wire session_id unchanged.\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\n                            let sid = remote_session.unwrap_or(session_id);\n                            intent = req_intent;\n                            attached_session = Some(sid);\n…\n                        }\n…\n                    }\n                }\n            }\n…\n            BrokerEvent::Subscribed { outcome, .. } => {\n…\n                match outcome {\n…\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n            BrokerEvent::Size { rows, cols, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            // This controller was kicked by a different operator's `--take`.\n            BrokerEvent::Displaced { by, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                // No dormant edge: the endpoint stays driven by the taker (whose\n                // own serve loop already fired the wake).\n                return Ok(AttachServeOutcome::Displaced);\n            }\n\n            // This VIEWER overflowed its broker subscription queue and was evicted\n            // (it forwards slower than the drain fans out under flood). SKIP TO\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\n            // spin. The next live Output carries a forward seq jump; the operator's\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n            BrokerEvent::ViewerEvicted { session_id: sid }\n                if attached && Some(sid) == attached_session =>\n            {\n                if let Some(prev) = last_resubscribe {\n                    let since = prev.elapsed();\n                    if since < RESUBSCRIBE_INTERVAL {\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\n                    }\n                }\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\n                last_resubscribe = Some(Instant::now());\n            }\n\n            // The operator closed its send side: the viewport is over.\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                wire.net_stream_send(stream_id, &[], None, true)?;\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n                // info.json, carrying the current driven_by through), THEN release\n                // the role over IPC so the broker's driven_by=None is the LAST\n                // write — otherwise a concurrent resting RMW restores the stale\n                // marker (the clear-race the broker-single-writer model averts).\n                if role == Some(ServeRole::Controller) {\n                    // The driver detached — the endpoint rests warm (controller-\n                    // only; a viewer leaving is wake-neutral).\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                if let Some(sid) = attached_session {\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\n                }\n                return Ok(AttachServeOutcome::Detached);\n            }\n\n            // ── Session → wire: output and exit. ────────────────────────────\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Output {\n                    seq,\n                    data_b64: encode_bytes(&bytes),\n                });\n                // Unjournaled by design (see module docs): `seq` rides inside\n                // the record, and the operator's render cursor is the dedup.\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            BrokerEvent::Exit { code, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Ok(AttachServeOutcome::Exited(code));\n            }\n\n            BrokerEvent::Error { message } => {\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Err(io::Error::other(message));\n            }\n            // Applied/NetSent acks and unrelated events: not ours to handle.\n            _ => {}\n        }\n    }\n}\n\n/// Operator-side: open an attach stream on a dialed connection and send the\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n/// durable id for the exactly-once stream open (safe to block for its ack —\n/// no attach traffic can be in flight before the stream exists); the Request\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\n/// and re-attaches idempotently).\n// [impl->REQ-INST-8]\npub fn request_attach(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n) -> io::Result<u64> {\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n}\n\n/// Like [`request_attach`], but for the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the remote operator cannot know the owning node's\n/// local session id, so it passes `endpoint_id = Some(endpoint)` (and a\n/// placeholder `session_id` of `0` the owning node ignores). The owning node\n/// resolves the endpoint to its OWN session id server-side\n/// ([`resolve_local_session`]) — authoritative for its own table. `None` here is\n/// exactly [`request_attach`]: the local path's wire bytes stay byte-identical.\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn request_attach_endpoint(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n    endpoint_id: Option<&str>,\n) -> io::Result<u64> {\n    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n    let line = ndjson::encode_line(&AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent,\n        endpoint_id: endpoint_id.map(str::to_string),\n    });\n    brain.net_stream_send(opened.stream_id, &line, None, false)?;\n    Ok(opened.stream_id)\n}\n\n/// Operator-side (CONTROLLER): send a resize record onto an attach stream —\n/// the controller's viewport size, forwarded into the target PTY (REQ-RCVIEW-1,\n/// controller-exclusive: the broker rejects a resize from a viewer's stream).\n/// Sent on attach (initial size) and on every window-change. Unjournaled (a\n/// stale resize is harmless — the next one corrects it).\n// [impl->REQ-RCVIEW-1]\npub fn send_attach_resize(\n    brain: &mut Brain,\n    stream_id: u64,\n    rows: u16,\n    cols: u16,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}\n\n/// Operator-side: send one input record (the keystrokes) onto an attach\n/// stream. `wire_op` is the operator-minted durable id the TARGET journals the\n/// PTY write under — exactly-once at the keys, however often the record\n/// crosses the wire. The send itself is unjournaled (see module docs).\n// [impl->REQ-INST-8]\npub fn send_attach_input(\n    brain: &mut Brain,\n    stream_id: u64,\n    bytes: &[u8],\n    wire_op: u64,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Input {\n        data_b64: encode_bytes(bytes),\n        op_id: wire_op,\n    });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}","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,null,158,null,162,null,165,166,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,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,null,382,null,424,425,426,null,429,null,431,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"}}},"isError":false,"timestamp":1784195850504}}
{"type":"message","id":"a607f904","parentId":"ebeda089","timestamp":"2026-07-16T09:57:30.514Z","message":{"role":"toolResult","toolCallId":"call_gFizVNRTuo4yRTaG3lEX81ps|fc_0698befac979932d016a58ab089ea4819aa79d5e19c7466b9f","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 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 89|            StartReason::Crash => \"crash\",\n*90|            StartReason::Update => \"update\",\n 91|        }\n 92|    }\n 93|\n...\n 98|        match s {\n*99|            \"update\" => StartReason::Update,\n 100|            \"crash\" => StartReason::Crash,\n 101|            _ => StartReason::Cold,\n 102|        }\n...\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 124|\n*125|    /// Request a planned restart (broker handler side) — idempotent: coalesces\n 126|    /// with any already-pending request into one cycle.\n 127|    pub fn request(&self) {\n 128|        self.requested.store(true, Ordering::SeqCst);\n...\n 285|        // window, so without this the wedge never drains: the promotion DRAINED gate\n*286|        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n 287|        // drives the evict) stays false for the full trial → \"alive but never ready\"\n 288|        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n 289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n...\n 481|    /// strands an apply where no old-gen control plane exists.\n*482|    fn old_gen_drained(&self) -> bool;\n 483|}\n 484|\n 485|/// The no-trial environment: every probe degrades to \"nothing pending / not\n...\n 505|    }\n*506|    fn old_gen_drained(&self) -> bool {\n 507|        // No readiness gate to drive here → no old-gen control plane to drain; the\n 508|        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n 509|        // no-op effects).\n...\n 519|/// Holds the live [`crate::broker::Broker`] handle (or `None` on a net-less /\n*520|/// bind-failed daemon) so the DRAINED gate ([`TrialEnv::old_gen_drained`]) reads\n 521|/// broker truth IN-PROCESS — the supervisor thread runs inside the broker process,\n 522|/// so this is a direct memory read, never an IPC round-trip through a possibly-\n 523|/// wedged conn (REQ-UPDATE-PROMOTE-DRAINED).\n...\n 571|    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n*572|    fn old_gen_drained(&self) -> bool {\n 573|        // Drained ⟺ no hosted session has a wedged LOCAL (brain-own) controller conn.\n 574|        // No broker (net-less / bind-failed) → no control plane → drained. A broker\n 575|        // with no wedged local controller (the common case, incl. no daemon-hosted\n...\n 671|/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n*672|/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n 673|/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n 674|/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n 675|/// the candidate and rolls back (conservative: never a false-promote onto a wedge).\n...\n 697|        }\n*698|        if ready_seen && env.old_gen_drained() {\n 699|            return TrialStep::Promoted;\n 700|        }\n 701|        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n...\n 750|    stop: &AtomicBool,\n*751|    restart: &BrainRestart,\n 752|    base: Duration,\n 753|    env: &dyn TrialEnv,\n 754|    window: Duration,\n...\n 887|                        if restart.take() {\n*888|                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n 889|                            let _ = child.kill();\n 890|                            let _ = child.wait();\n 891|                            planned = true;\n...\n 918|        if planned {\n*919|            reason = StartReason::Update;\n 920|            backoff = base;\n 921|            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n 922|            continue;\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#### 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#### deadline.rs\n 13|//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n*14|//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n 15|//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n 16|//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n 17|//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n...\n 80|    ///\n*81|    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n 82|    ///   only the interval refreshes from live config. If no file exists (updating\n 83|    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n 84|    ///   — never fail an update on a missing file.\n...\n 101|            // Update keeps the persisted phase; the interval refreshes to config.\n*102|            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103|                anchor_ms: prev.anchor_ms,\n 104|                interval_ms,\n 105|            },\n...\n 316|            // Update preserves phase (anchor) but the interval tracks live config.\n*317|            let upd = DeadlineAnchor::open(\"pulse\", 250, StartReason::Update, 9_999).unwrap();\n 318|            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n 319|            assert_eq!(upd.interval_ms, 250, \"update must refresh the interval\");\n 320|        });\n...\n 326|            // No prior anchor (e.g. updating FROM a pre-D5 binary) → fresh, not a fail.\n*327|            let upd = DeadlineAnchor::open(\"pulse\", 100, StartReason::Update, 7_000).unwrap();\n 328|            assert_eq!(upd.anchor_ms, 7_000);\n 329|        });\n 330|    }\n...\n 336|            // A garbled file is treated as absent → Update falls back to fresh.\n*337|            let upd = DeadlineAnchor::open(\"pulse\", 100, StartReason::Update, 8_000).unwrap();\n 338|            assert_eq!(upd.anchor_ms, 8_000);\n 339|        });\n 340|    }\n...\n 349|            // Re-open A in update mode → its own anchor survived B's write.\n*350|            let a = DeadlineAnchor::open(\"agent-a\", 100, StartReason::Update, 9_999).unwrap();\n 351|            assert_eq!(a.anchor_ms, 1_000, \"agent-a's phase must survive agent-b's write\");\n 352|            assert_ne!(anchor_path(\"agent-a\"), anchor_path(\"agent-b\"));\n 353|        });\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#### lifecycle.rs\n 2363|            // Update keeps the same grid phase (no re-base) even across a restart.\n*2364|            run(StartReason::Update);\n 2365|            assert_eq!(anchor_of(), cold_phase, \"Update must preserve the grid phase\");\n 2366|\n 2367|            // A crash restart re-bases the anchor to a fresh instant.\n#### msg.rs\n 874|/// cycle the child; false when the broker has none wired (a bare test broker),\n*875|/// so the caller can tell a real planned restart from a mere accept.\n 876|#[derive(Debug, Clone, Serialize, Deserialize)]\n*877|pub struct BrainRestarted {\n 878|    #[serde(default)]\n 879|    pub honored: bool,\n 880|}\n\n### tests/\n#### false_promote.rs\n 13|//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n*14|//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n 15|//! read from BROKER TRUTH in-process (no brain round-trip).\n 16|//!\n 17|//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n...\n 40|//! 1. the trial does NOT promote while wedged (RED-first: remove the\n*41|//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n 42|//!    reproducing the 22:47 false-promote);\n 43|//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n 44|//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n...\n 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 229|    // The gate under test: REAL broker truth, no brain round-trip.\n*230|    fn old_gen_drained(&self) -> bool {\n 231|        !self.broker.any_local_controller_wedged()\n 232|    }\n 233|}\n...\n 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/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...\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### resume_no_control_steal_e2e.rs\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 282|/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n*283|/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n 284|/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n 285|/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\n 286|struct ResumeTrialEnv {\n...\n 323|    // No wedge in this rig → drained is true → the latched-ready candidate promotes.\n*324|    fn old_gen_drained(&self) -> bool {\n 325|        !self.broker.any_local_controller_wedged()\n 326|    }\n 327|}\n...\n 399|        thread::spawn(move || {\n*400|            let restart = BrainRestart::new();\n 401|            supervise_brain(\n 402|                &stop_sup,\n 403|                &restart,\n...\n 502|    // controllers must not break REQ-UPDATE-TRIAL-DRAIN-DRIVE. With no controller wedged,\n*503|    // old_gen_drained() is true and the ready candidate promotes.\n 504|    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 505|    assert!(\n 506|        promoted,\n...\n 508|         REQ-UPDATE-TRIAL-DRAIN-DRIVE): with the Viewer re-attach no controller is wedged, so \\\n*509|         old_gen_drained() is true and the ready candidate promotes. rollbacks={rollbacks:?}\"\n 510|    );\n 511|    assert_eq!(\n 512|        promotions.as_slice(),\n### trial_drain_drive_e2e.rs\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...\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 67|//!   before commit): comment out the production drive `let _ = brain.sessions();`\n 68|//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\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 319|    // The gate under drive: REAL broker truth, no brain round-trip.\n*320|    fn old_gen_drained(&self) -> bool {\n 321|        !self.broker.any_local_controller_wedged()\n 322|    }\n 323|}\n...\n 373|        thread::spawn(move || {\n*374|            let restart = BrainRestart::new();\n 375|            supervise_brain(\n 376|                &stop_sup,\n 377|                &restart,\n...\n 447|    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\n*448|    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\n 449|    //    masking the very regression the RED must catch. ──\n 450|    let baseline_evicts = read_evicts(&broker_socket_name());\n 451|    let form_deadline = Instant::now() + Duration::from_secs(30);\n...\n 473|    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\n*474|    //    commented out): nothing reaps it → old_gen_drained stays false → the window\n 475|    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\n 476|    env.armed.store(true, Ordering::Relaxed);\n 477|\n...\n 516|    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\n*517|    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\n 518|    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n 519|    assert!(\n 520|        promoted,\n 521|        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n*522|         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n 523|         the window. RED (comment the production `let _ = brain.sessions();` in \\\n 524|         brainproc.rs, rebuild): nothing drives the reap → the wedge strands the DRAINED \\\n 525|         gate → WindowElapsedAlive → rollback (the 2026-07-09 field freeze).\\n\\"}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt-daemon/tests, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":78,"fileCount":13,"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/deadline.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt/tests/brain_survive.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":13},{"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/deadline.rs","count":7},{"path":"crates/spt-daemon/src/lib.rs","count":1},{"path":"crates/spt-daemon/src/lifecycle.rs","count":1},{"path":"crates/spt-daemon/src/msg.rs","count":2},{"path":"crates/spt-daemon/tests/false_promote.rs","count":5},{"path":"crates/spt/tests/brain_survive.rs","count":3},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":7},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":10}],"truncated":true,"perFileLimitReached":20,"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 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   89│            StartReason::Crash => \"crash\",\n  *90│            StartReason::Update => \"update\",\n   91│        }\n   92│    }\n   93│\n     │...\n   98│        match s {\n  *99│            \"update\" => StartReason::Update,\n  100│            \"crash\" => StartReason::Crash,\n  101│            _ => StartReason::Cold,\n  102│        }\n     │...\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  124│\n *125│    /// Request a planned restart (broker handler side) — idempotent: coalesces\n  126│    /// with any already-pending request into one cycle.\n  127│    pub fn request(&self) {\n  128│        self.requested.store(true, Ordering::SeqCst);\n     │...\n  285│        // window, so without this the wedge never drains: the promotion DRAINED gate\n *286│        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n  287│        // drives the evict) stays false for the full trial → \"alive but never ready\"\n  288│        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n  289│        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n     │...\n  481│    /// strands an apply where no old-gen control plane exists.\n *482│    fn old_gen_drained(&self) -> bool;\n  483│}\n  484│\n  485│/// The no-trial environment: every probe degrades to \"nothing pending / not\n     │...\n  505│    }\n *506│    fn old_gen_drained(&self) -> bool {\n  507│        // No readiness gate to drive here → no old-gen control plane to drain; the\n  508│        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n  509│        // no-op effects).\n     │...\n  519│/// Holds the live [`crate::broker::Broker`] handle (or `None` on a net-less /\n *520│/// bind-failed daemon) so the DRAINED gate ([`TrialEnv::old_gen_drained`]) reads\n  521│/// broker truth IN-PROCESS — the supervisor thread runs inside the broker process,\n  522│/// so this is a direct memory read, never an IPC round-trip through a possibly-\n  523│/// wedged conn (REQ-UPDATE-PROMOTE-DRAINED).\n     │...\n  571│    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n *572│    fn old_gen_drained(&self) -> bool {\n  573│        // Drained ⟺ no hosted session has a wedged LOCAL (brain-own) controller conn.\n  574│        // No broker (net-less / bind-failed) → no control plane → drained. A broker\n  575│        // with no wedged local controller (the common case, incl. no daemon-hosted\n     │...\n  671│/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n *672│/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n  673│/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n  674│/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n  675│/// the candidate and rolls back (conservative: never a false-promote onto a wedge).\n     │...\n  697│        }\n *698│        if ready_seen && env.old_gen_drained() {\n  699│            return TrialStep::Promoted;\n  700│        }\n  701│        // Only watch for a pre-ready exit while the candidate has NOT reached ready;\n     │...\n  750│    stop: &AtomicBool,\n *751│    restart: &BrainRestart,\n  752│    base: Duration,\n  753│    env: &dyn TrialEnv,\n  754│    window: Duration,\n     │...\n  887│                        if restart.take() {\n *888│                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n  889│                            let _ = child.kill();\n  890│                            let _ = child.wait();\n  891│                            planned = true;\n     │...\n  918│        if planned {\n *919│            reason = StartReason::Update;\n  920│            backoff = base;\n  921│            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n  922│            continue;\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#### 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#### deadline.rs\n  13│//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n *14│//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n  15│//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n  16│//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n  17│//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n    │...\n  80│    ///\n *81│    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n  82│    ///   only the interval refreshes from live config. If no file exists (updating\n  83│    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n  84│    ///   — never fail an update on a missing file.\n    │...\n 101│            // Update keeps the persisted phase; the interval refreshes to config.\n*102│            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103│                anchor_ms: prev.anchor_ms,\n 104│                interval_ms,\n 105│            },\n    │...\n 316│            // Update preserves phase (anchor) but the interval tracks live config.\n*317│            let upd = DeadlineAnchor::open(\"pulse\", 250, StartReason::Update, 9_999).unwrap();\n 318│            assert_eq!(upd.anchor_ms, 1_000, \"update must preserve the grid phase\");\n 319│            assert_eq!(upd.interval_ms, 250, \"update must refresh the interval\");\n 320│        });\n    │...\n 326│            // No prior anchor (e.g. updating FROM a pre-D5 binary) → fresh, not a fail.\n*327│            let upd = DeadlineAnchor::open(\"pulse\", 100, StartReason::Update, 7_000).unwrap();\n 328│            assert_eq!(upd.anchor_ms, 7_000);\n 329│        });\n 330│    }\n    │...\n 336│            // A garbled file is treated as absent → Update falls back to fresh.\n*337│            let upd = DeadlineAnchor::open(\"pulse\", 100, StartReason::Update, 8_000).unwrap();\n 338│            assert_eq!(upd.anchor_ms, 8_000);\n 339│        });\n 340│    }\n    │...\n 349│            // Re-open A in update mode → its own anchor survived B's write.\n*350│            let a = DeadlineAnchor::open(\"agent-a\", 100, StartReason::Update, 9_999).unwrap();\n 351│            assert_eq!(a.anchor_ms, 1_000, \"agent-a's phase must survive agent-b's write\");\n 352│            assert_ne!(anchor_path(\"agent-a\"), anchor_path(\"agent-b\"));\n 353│        });\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#### lifecycle.rs\n 2363│            // Update keeps the same grid phase (no re-base) even across a restart.\n*2364│            run(StartReason::Update);\n 2365│            assert_eq!(anchor_of(), cold_phase, \"Update must preserve the grid phase\");\n 2366│\n 2367│            // A crash restart re-bases the anchor to a fresh instant.\n#### msg.rs\n 874│/// cycle the child; false when the broker has none wired (a bare test broker),\n*875│/// so the caller can tell a real planned restart from a mere accept.\n 876│#[derive(Debug, Clone, Serialize, Deserialize)]\n*877│pub struct BrainRestarted {\n 878│    #[serde(default)]\n 879│    pub honored: bool,\n 880│}\n\n### tests/\n#### false_promote.rs\n  13│//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n *14│//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n  15│//! read from BROKER TRUTH in-process (no brain round-trip).\n  16│//!\n  17│//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n    │...\n  40│//! 1. the trial does NOT promote while wedged (RED-first: remove the\n *41│//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n  42│//!    reproducing the 22:47 false-promote);\n  43│//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n  44│//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n    │...\n  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 229│    // The gate under test: REAL broker truth, no brain round-trip.\n*230│    fn old_gen_drained(&self) -> bool {\n 231│        !self.broker.any_local_controller_wedged()\n 232│    }\n 233│}\n    │...\n 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/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    │...\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### resume_no_control_steal_e2e.rs\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 282│/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n*283│/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n 284│/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n 285│/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\n 286│struct ResumeTrialEnv {\n    │...\n 323│    // No wedge in this rig → drained is true → the latched-ready candidate promotes.\n*324│    fn old_gen_drained(&self) -> bool {\n 325│        !self.broker.any_local_controller_wedged()\n 326│    }\n 327│}\n    │...\n 399│        thread::spawn(move || {\n*400│            let restart = BrainRestart::new();\n 401│            supervise_brain(\n 402│                &stop_sup,\n 403│                &restart,\n    │...\n 502│    // controllers must not break REQ-UPDATE-TRIAL-DRAIN-DRIVE. With no controller wedged,\n*503│    // old_gen_drained() is true and the ready candidate promotes.\n 504│    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 505│    assert!(\n 506│        promoted,\n    │...\n 508│         REQ-UPDATE-TRIAL-DRAIN-DRIVE): with the Viewer re-attach no controller is wedged, so \\\n*509│         old_gen_drained() is true and the ready candidate promotes. rollbacks={rollbacks:?}\"\n 510│    );\n 511│    assert_eq!(\n 512│        promotions.as_slice(),\n### trial_drain_drive_e2e.rs\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    │...\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  67│//!   before commit): comment out the production drive `let _ = brain.sessions();`\n  68│//!   (the impl-tagged line in brainproc.rs run_brain's heartbeat loop), rebuild `spt`, and this\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 319│    // The gate under drive: REAL broker truth, no brain round-trip.\n*320│    fn old_gen_drained(&self) -> bool {\n 321│        !self.broker.any_local_controller_wedged()\n 322│    }\n 323│}\n    │...\n 373│        thread::spawn(move || {\n*374│            let restart = BrainRestart::new();\n 375│            supervise_brain(\n 376│                &stop_sup,\n 377│                &restart,\n    │...\n 447│    //    (the GREEN heartbeat reaped it between our polls). Without this gate a RED build\n*448│    //    could promote trivially against an un-formed wedge (old_gen_drained true) —\n 449│    //    masking the very regression the RED must catch. ──\n 450│    let baseline_evicts = read_evicts(&broker_socket_name());\n 451│    let form_deadline = Instant::now() + Duration::from_secs(30);\n    │...\n 473│    //    heartbeat sessions() drive reaps it (GREEN). RED (production brain.sessions()\n*474│    //    commented out): nothing reaps it → old_gen_drained stays false → the window\n 475│    //    elapses → WindowElapsedAlive → rollback (the field freeze). ──\n 476│    env.armed.store(true, Ordering::Relaxed);\n 477│\n    │...\n 516│    // Leg 1 (load-bearing): the candidate's OWN self-drive reaped the matured old-gen\n*517│    // wedge → old_gen_drained flipped → the latched ready promoted within the window.\n 518│    // [int->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n 519│    assert!(\n 520│        promoted,\n 521│        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n*522│         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n 523│         the window. RED (comment the production `let _ = brain.sessions();` in \\\n 524│         brainproc.rs, rebuild): nothing drives the reap → the wedge strands the DRAINED \\\n 525│         gate → WindowElapsedAlive → rollback (the 2026-07-09 field freeze).\\n\\"},"isError":false,"timestamp":1784195850514}}
{"type":"message","id":"651c6b95","parentId":"a607f904","timestamp":"2026-07-16T09:57:30.533Z","message":{"role":"toolResult","toolCallId":"call_qOw333m6LciF6inARE8v1HFB|fc_0698befac979932d016a58ab089f2c819a9c4aa0e6e325c767","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n\n### tests/\n#### attach.rs\n 1204|//\n*1205|// [int->REQ-HAZARD-CONTROLLER-GAP-RESUME] The controller is driven through\n 1206|// `read_controller_event_resuming`, NOT raw `read_event`, because the b4\n 1207|// drop-don't-block fix decoupled controller backpressure from the drain: the\n 1208|// controller is now a NON-BLOCKING try_send that DROPS on a full channel\n...\n 1212|// surfaces to the strict controller cursor as an `output gap`. The helper\n*1213|// RESUMES-FROM-FLOOR on that gap (re-fetch from `controller_resume_floor`, B2 — a\n 1214|// re-fetch, not a viewer-style snap).\n 1215|//\n 1216|// VIEWER-ISOLATION asserts the wedged viewer does not STALL the controller — and a\n#### brain_decouple.rs\n 62|    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n*63|    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n 64|};\n 65|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n 66|use spt_daemon::Broker;\n...\n 195|/// Query the broker's stall-evict tally.\n*196|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197|    let name = name.to_string();\n 198|    let (tx, rx) = std::sync::mpsc::channel();\n 199|    thread::spawn(move || {\n 200|        let mut c = connect(&name);\n*201|        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 202|        loop {\n 203|            match read_frame(&mut c) {\n*204|                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 205|                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 206|                    let _ = tx.send(r.count);\n 207|                    return;\n...\n 221|#[test]\n*222|fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223|    init_home();\n 224|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225|    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n...\n 335|\n*336|    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n 337|\n 338|    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 339|    if let Some(pid) = broker.session_pid(sid) {\n...\n 344|        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n*345|         take_elapsed={took:?} stall_evicts={evicts} ===\"\n 346|    );\n 347|\n 348|    // The viewer kept ticking while the controller conn was wedged.\n...\n 383|// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\n*384|// connection: `stall_evict_controller` drops the `ControllerSink` (one\n 385|// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\n 386|// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\n 387|// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\n...\n 395|// This test reproduces that EXACT Windows failure mode with a real PTY child and\n*396|// a genuinely black-holed controller connection, then asserts the two things the\n 397|// logical gate cannot see:\n*398|//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\n 399|//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\n 400|//     client ever dropping its end;                                    ← RED pre-fix\n 401|//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\n...\n 416|/// `seq>0` controller frame; the burst floods a non-draining controller's socket\n*417|/// so its writer parks INSIDE `write_frame` — the exact black-hole the RCA pinned.\n 418|/// Sibling of [`flood_spawn_req`], but FINITE + input-triggered (the flood child\n 419|/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\n 420|#[cfg(windows)]\n...\n 442|/// One `KIND_SESSIONS` round-trip, returning the full reply. The dispatch runs\n*443|/// `reap_dead_controller` → the time-based `stall_evict_controller`, so polling\n 444|/// this both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 445|/// Bounded thread + channel (a wedge surfaces as a timeout, not a hang), like the\n*446|/// existing [`stall_evict_count`] helper. The transient conn is dropped when the\n 447|/// thread returns.\n 448|#[cfg(windows)]\n 449|fn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {\n...\n 473|#[test]\n*474|fn non_draining_controller_stall_evict_releases_writer_and_connection() {\n 475|    init_home();\n 476|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\n 477|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n...\n 486|    // ── Step 2: spawn the real SEED+burst PTY child from conn A, then DROP A so the\n*487|    //    ONLY persistent brain conn later is the black-holed controller R. ──\n 488|    let mut a = connect(&name);\n 489|    send(\n 490|        &mut a,\n...\n 581|        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame is\n*582|        // proven delivered, then STOP reading forever (the black-hole). The remaining\n 583|        // ~1300 frames flood the now-unread socket → the R-writer wedges in write_frame.\n 584|        let post_deadline = Instant::now() + Duration::from_secs(5);\n 585|        let mut post = 0u32;\n...\n 597|        }\n*598|        // Hand the Stream back to main UNREAD (main holds it = the black-holed client).\n 599|        let _ = hand_tx.send((r, max_seq));\n 600|        // Thread exits; the broker's R-writer stays wedged on the remaining burst.\n 601|    });\n...\n 609|    let _ = go_tx.send(());\n*610|    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 611|    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n 612|    let mut r_stream: Option<Stream> = None;\n 613|    let mut max_drained_seq: u64 = 0;\n...\n 639|    // ONE wedged controller's release can be tallied by the LOGICAL reap\n*640|    // (stall_evict_controller), by the PHYSICAL bound (the conn watchdog poisons\n 641|    // first and note_controller_write_retired records the same event class —\n 642|    // the field-accepted v0.30.6 behavior), or by BOTH when the watchdog races\n 643|    // the reap's blocked-window sample. So the tally can step 0→2 and an\n...\n 646|    // real second eviction and still fails.\n*647|    let mut stall_evicts_seen: u64 = 0;\n 648|    let mut t_release: Option<Instant> = None;\n 649|    let mut frozen_cursor: Option<u64> = None;\n 650|    let step6_deadline = Instant::now() + Duration::from_secs(10);\n#### conn_blackhole_lifecycle.rs\n*1|//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n 2|//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n 3|//! hosting REAL PTY children over the real local IPC.\n 4|//!\n...\n 9|//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n*10|//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n 11|//! finite burst floods its socket, so the broker's writer parks inside\n 12|//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n 13|//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n...\n 17|//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n*18|//!      receiving output throughout the black-hole incident (the fan-out never\n 19|//!      blocks the whole broker on the one wedged conn);\n 20|//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n 21|//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n...\n 25|//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n*26|//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n 27|//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n 28|//!      completion), and the viewer flows output AFTER the sample latches — proving\n 29|//!      the survivor is the viewer's handler, not a leaked one;\n...\n 45|//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n*46|//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n 47|//! Windows-mandatory physical-retirement leg for\n 48|//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n 49|//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n 50|//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n*51|//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n 52|//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n 54|//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n...\n 59|//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n*60|//! asserts, and the deterministic black-hole only reproduces on the Windows\n 61|//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n 62|//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n 63|//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n...\n 91|    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n*92|    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n*93|    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n 94|};\n 95|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n 96|use spt_daemon::Broker;\n...\n 136|/// session so its viewer has a continuous frame stream to keep ticking on\n*137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n 140|        program: \"cmd\".to_string(),\n...\n 159|/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n*160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163|    SpawnReq {\n...\n 197|/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199|    let name = name.to_string();\n 200|    let (tx, rx) = std::sync::mpsc::channel();\n 201|    thread::spawn(move || {\n 202|        let mut c = connect(&name);\n*203|        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 204|        loop {\n 205|            match read_frame(&mut c) {\n*206|                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 207|                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 208|                    let _ = tx.send(r.count);\n 209|                    return;\n...\n 222|/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n...\n 291|\n*292|    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293|    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294|    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295|    drop(a);\n 296|\n...\n 371|        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n*372|        // is proven delivered, then STOP reading forever (the black-hole). The\n 373|        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n 374|        let post_deadline = Instant::now() + Duration::from_secs(5);\n 375|        let mut post = 0u32;\n...\n 386|        }\n*387|        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388|        // client). The broker's R-writer stays wedged on the remaining burst.\n 389|        let _ = hand_tx.send((r, max_seq));\n 390|    });\n...\n 398|    let _ = go_tx.send(());\n*399|    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 400|    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n 401|    let mut r_stream: Option<Stream> = None;\n 402|    let mut max_drained_seq: u64 = 0;\n#### false_promote.rs\n 19|//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n*20|//! `write_frame` on a black-holed spawner conn. Round-4\n 21|//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n 22|//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n 23|//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n...\n 32|//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n*33|//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n 34|//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n 35|//! wall-clock only; it never relaxes an assertion).\n 36|//!\n#### inject_control_wedge.rs\n 522|        std::env::set_var(\"SPT_HOME\", dir.path());\n*523|        // W5-A: shrink the settle-gate timeout suite-wide. The mock PTY children here\n 524|        // (findstr/cat) never answer the DSR readiness probe, so the settle-gate always\n 525|        // elapses its bounded wait once per worker — its PRESENCE is the invariant, not\n 526|        // the length, so 80ms keeps the suite fast and removes the fixed-sleep timing\n#### net_worker_starve.rs\n 10|//! STAGE 1 (this file, first): the narrow mechanism probe. Stand a dialer whose\n*11|//! connects black-hole a dead peer, fire a `submit_dial` burst straight onto the\n 12|//! broker's own [`NetHost`] (the exact pump path), and read the two round-3\n 13|//! DIAGNOSTIC probes added to `nethost.rs`:\n 14|//!   - `active_dial_tasks()` — in-flight dead-peer connects on the 2 workers;\n...\n 106|/// THE DECIDER (round-3 ruling §5): a burst of `submit_dial`s to a peer that\n*107|/// accepts the QUIC handshake but black-holes the seed-proof. Reports whether\n 108|/// the burst holds the 2 workers (canary goes stale) or the awaits yield (canary\n 109|/// stays fresh). This test does not FAIL on either outcome — it PRINTS the verdict\n 110|/// the ruling needs; an explicit assert nails the direction once observed.\n...\n 121|\n*122|    // B: the black hole — accepts the handshake, never runs the proof responder.\n 123|    let broker_b = served_broker(&name_b, hermetic(Identity::generate()), &dir.path().join(\"b\"));\n 124|    let host_b = broker_b.net().expect(\"host b\");\n 125|    let b_addr = host_b.addr();\n#### netbroker.rs\n 53|/// is irrelevant; its PRESENCE is what makes the dialer open the stream and\n*54|/// block, giving the deterministic black hole the broker-side bound must cut.\n 55|fn proving(identity: Identity) -> NetConfig {\n 56|    let mut cfg = hermetic(identity);\n 57|    cfg.membership = Some(std::sync::Arc::new(|| {\n...\n 234|// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n*235|// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236|// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237|// error reply within the bound, NEVER an unbounded await. Exactly-once is\n 238|// preserved on timeout: the journaled dial records NO phantom conn — the journal\n...\n 260|\n*261|    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it\n 262|    // registers the conn) but never runs the proof responder, so A's\n 263|    // `prove_membership` awaits a control-stream reply that never comes.\n 264|    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));\n...\n 273|        .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))\n*274|        .expect_err(\"a black-holed dial must error, never block forever\");\n 275|    let elapsed = started.elapsed();\n 276|\n 277|    // The broker REPLIED an ordinary error within the bound — not a wedge, and\n#### pump.rs\n 67|/// dial runs `prove_membership`, which a `membership: None` peer never answers\n*68|/// (the documented mixed-pair deadlock) — the deterministic black hole the\n 69|/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its\n 70|/// PRESENCE is what makes the dialer open the proof control stream and block.\n 71|fn proving() -> NetConfig {\n...\n 505|\n*506|    // B: the black hole — accepts the handshake (`membership: None`) but never\n 507|    // runs the proof responder, so A's dial hangs in `prove_membership` until the\n 508|    // broker-side bound cuts it.\n 509|    let _b_broker = net_broker(&b_name, dir.path());\n...\n 645|/// hardcodes `hermetic()`; W2 needs a `proving()` LIVE peer alongside hermetic\n*646|/// black holes). Serve thread detached; effect log named per broker.\n 647|fn net_broker_with(name: &str, dir: &std::path::Path, cfg: NetConfig) -> Arc<Broker> {\n 648|    let host = NetHost::start(cfg).expect(\"net host start\");\n 649|    let broker = Broker::bind_in_with_net(name, dir.join(format!(\"{name}-eff.log\")), Some(host))\n...\n 712|\n*713|    // A's stores: the subnet whose roster is the live peer + the two black holes.\n 714|    let a_root = dir.path().join(\"a\");\n 715|    std::fs::create_dir_all(&a_root).unwrap();\n 716|    let mut a_subnets = SubnetStore::default();\n#### pumpdeadline.rs\n 10|//! The wedge this guards: the 2.2h hfenduleam pump stall on\n*11|//! `net_open_stream`'s unbounded `loop { read_event }` against a black-holed\n 12|//! peer the broker never answered for.\n 13|\n 14|use std::io;\n\n### src/\n#### attach.rs\n 151|/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\n*152|/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\n 153|/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\n 154|/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n 155|/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n#### brain.rs\n 45|    ViewerEvictedEvent,\n*46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n 47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n 48|    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n 49|    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n...\n 194|/// **Why the split exists (REQ-HAZARD-PUMP-IPC-DEADLINE).** The pump is\n*195|/// single-threaded; a brain-IPC read that blocks forever on a black-holed peer\n 196|/// wedges the whole loop (the 2.2h hfenduleam stall). The deadline must be\n 197|/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n 198|/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n...\n 297|    /// brain-IPC read waits forever, which is correct everywhere EXCEPT the\n*298|    /// single-threaded pump, where a black-holed peer would wedge the whole loop\n 299|    /// (KNOWN-HAZARDS — a blocked brain-IPC read must never wedge the pump). The\n 300|    /// two facts are coupled at construction: `Some` ⇔ [`BrainConn::Split`].\n 301|    io_timeout: Option<Duration>,\n...\n 339|    /// every reply read is bounded ([`Brain::read_event_until`] via\n*340|    /// [`Brain::call_deadline`]) so a black-holed peer can never wedge the\n 341|    /// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a\n 342|    /// live `Whole` after the fact — moving the `Stream` out by value to split it\n 343|    /// is impossible behind `&mut self`), so a pump brain is split-and-bounded\n...\n 1155|    /// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): `(count,\n*1156|    /// last_ms)` — how many controllers were stall-evicted for a suspended/black-holed\n 1157|    /// brain conn since boot, and when the most recent fired. `None` when the broker\n 1158|    /// is too old to answer (an older broker replies `unknown command kind`), read as\n 1159|    /// \"not reported\" on `daemon status` — same forward-compat contract as\n...\n 1161|    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1162|    pub fn stall_evicts(&mut self) -> io::Result<Option<(u64, u64)>> {\n*1163|        self.send(KIND_STALL_EVICTS, serde_json::Value::Null)?;\n 1164|        loop {\n 1165|            let env = self.read_frame_until(None)?;\n 1166|            match env.kind.as_str() {\n*1167|                KIND_STALL_EVICTS_REPLY => {\n 1168|                    let ev: StallEvictsReply = serde_json::from_value(env.payload)\n 1169|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1170|                    return Ok(Some((ev.count, ev.last_ms)));\n...\n 1599|    /// does not close the conn), FREEZING the session immediately; a >15s\n*1600|    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n 1601|    /// (only an actively-producing stolen session blocks long enough to evict).\n 1602|    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n 1603|    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n...\n 1915|    /// KEYSTONE — the brain-level primitive the `serve_attach` controller\n*1916|    /// gap-resume (REQ-HAZARD-CONTROLLER-GAP-RESUME) is built on: a CONTROLLER on\n 1917|    /// the strict legacy reject-gap path (empty `session_cursors`) that FATALS a b4\n 1918|    /// drop-don't-block output-gap can RECOVER by re-subscribing from\n 1919|    /// [`Brain::controller_resume_floor`] (== `next_seq` == the gap error's `want`\n#### brainproc.rs\n 280|        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n*281|        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n 282|        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283|        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n 284|        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n...\n 287|        // drives the evict) stays false for the full trial → \"alive but never ready\"\n*288|        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n*289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n 290|        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291|        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292|        // net_status probe above already owns broker-liveness). Self-applies: it drives\n#### broker.rs\n 64|    KIND_ADAPTER_APPLY, KIND_APPLIED,\n*65|    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n 66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n 67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n...\n 156|/// How long a controller's writer thread may sit BLOCKED inside a single socket\n*157|/// write — the brain subscriber suspended / black-holed — before a take/reattach\n 158|/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n 159|/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n 160|/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n...\n 254|/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n*255|/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n 256|/// line, not a silent absence.\n 257|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*258|static STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\n*259|static STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n 260|\n 261|/// Record one stall-evict in the broker-global tally (best-effort observability).\n 262|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*263|fn record_stall_evict() {\n*264|    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n*265|    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 266|}\n 267|\n 268|/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n...\n 271|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*272|pub(crate) fn stall_evict_stats() -> (u64, u64) {\n 273|    (\n*274|        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n*275|        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n 276|    )\n 277|}\n 278|\n...\n 515|    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n*516|    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n 517|    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n 518|    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n 519|    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n...\n 577|    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**\n*578|    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).\n 579|    ///\n 580|    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**\n 581|    /// (never decreases) — see [`advanced_cursor`]. Both invariants are\n...\n 700|    /// --attach` case #6 is about (attach to an already-producing TUI). A\n*701|    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n 702|    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n 703|    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n 704|    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n...\n 729|    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n*730|    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n 731|    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n 732|    /// (bounded wedge preserved). The writer still does the blocking socket write +\n 733|    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n...\n 1008|        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n*1009|        self.stall_evict_controller(Instant::now(), StampMode::Deferred)\n 1010|    }\n 1011|\n 1012|    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and\n...\n 1023|    /// socket write for at least `deadline` (the brain-subscriber conn suspended /\n*1024|    /// black-holed). An IDLE controller (writer parked on `rx.recv()`, nothing to\n 1025|    /// write) reads NOT stalled: its [`ControllerSink::write_blocked_since`] is `None`.\n 1026|    /// Pure over the injected `now` + `deadline` so the unit tests it with no thread.\n 1027|    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n...\n 1040|    /// it (REQ-UPDATE-PROMOTE-DRAINED). A killed OLD brain generation whose local\n*1041|    /// controller conn is black-holed reads wedged here until the W2 stall-evict\n 1042|    /// releases it (or the conn EOFs); a cleanly-closed conn is simply absent. A\n 1043|    /// REMOTE operator's wedged conn (`by: Some`) is the W2 stall-evict concern, NOT\n 1044|    /// an old-gen brain conn, so it is EXCLUDED — a stuck remote viewer must never\n...\n 1063|    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1064|    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n 1065|        let deadline = brain_write_deadline();\n 1066|        if !self.controller_write_stalled(now, deadline) {\n 1067|            return false;\n...\n 1072|        eprintln!(\n*1073|            \"BRAIN_SUBSCRIBER_STALL_EVICT:{} conn={conn} {} — controller writer blocked > {}ms \\\n#### conn.rs\n 11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n...\n 780|        // Poisoned + retired: the next write refuses immediately.\n*781|        let refused = conn.write(&env).expect_err(\"poisoned conn refuses writes\");\n 782|        assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);\n 783|    }\n 784|\n#### msg.rs\n 103|/// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — how many\n*104|/// controllers were stall-evicted for a suspended/black-holed brain conn since\n 105|/// boot, and when the most recent fired. Surfaced on `daemon status` so a brain\n 106|/// that never returns is a diagnosable fact. An older broker predating this kind\n 107|/// replies `unknown command kind` (forward-compat), read as \"not reported\".\n*108|pub const KIND_STALL_EVICTS: &str = \"stall-evicts\";\n 109|/// Broker→brain: the stall-evict tally reply.\n*110|pub const KIND_STALL_EVICTS_REPLY: &str = \"stall-evicts-reply\";\n 111|/// Brain→broker: dial a peer over the broker-owned endpoint (D4a). With an\n 112|/// `op_id` the dial is exactly-once (journaled like an idempotent `input`).\n 113|pub const KIND_NET_DIAL: &str = \"net-dial\";\n...\n 345|    /// handshake-proven origin), `None` = local (the daemon brain's own\n*346|    /// spawn-reader / resume). The broker keys controller identity on this:\n 347|    /// a same-`by` re-subscribe silently re-takes the controller (no\n 348|    /// `Displaced`); a `None` (local) re-attach NEVER displaces a remote\n 349|    /// controller (it falls back to viewer) — the gate-#7 invariant.\n...\n 557|pub struct StallEvictsReply {\n*558|    /// Controllers stall-evicted for a suspended/black-holed brain conn since broker boot.\n 559|    #[serde(default)]\n 560|    pub count: u64,\n 561|    /// Wall-clock epoch-ms of the most recent stall-evict (0 = none since boot).\n#### nethost.rs\n 59|/// brain-waiting QUIC await — `dial` / `open_stream` / `send_stream` — is bounded\n*60|/// by this so a black-holed or dead peer fails PROMPTLY with an ORDINARY error\n 61|/// the broker replies, never an unbounded await that wedges the single-threaded\n 62|/// pump (the B-half of REQ-HAZARD-PUMP-IPC-DEADLINE; the 2.2h hfenduleam wedge).\n 63|/// Chosen at **10s**: comfortably above any legitimate LAN/relay connect +\n...\n 70|/// frame's IPC transit + the brain's read scheduling. Test-overridable via\n*71|/// [`NetHost::set_quic_op_timeout`] so the hermetic black-hole tests fail fast.\n 72|const BROKER_QUIC_OP_TIMEOUT_MS: u64 = 10_000;\n 73|\n 74|/// Bounded concurrent-dial cap (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G4): a\n...\n 759|    /// The broker-side QUIC-op deadline in ms (REQ-HAZARD-BROKER-QUIC-DEADLINE),\n*760|    /// default [`BROKER_QUIC_OP_TIMEOUT_MS`]; the hermetic black-hole tests lower\n 761|    /// it via [`Self::set_quic_op_timeout`]. Interior-mutable so the setter takes\n 762|    /// `&self`, like the live host's other post-`start` knobs (`set_roster_exchange`).\n 763|    quic_op_timeout_ms: AtomicU64,\n...\n 967|    /// Production keeps the [`BROKER_QUIC_OP_TIMEOUT_MS`] default; the hermetic\n*968|    /// black-hole tests set a short bound so a dead-peer await fails fast instead\n 969|    /// of pinning the test for ~10s. Off [`NetConfig`] by design (mirrors\n 970|    /// [`Self::set_roster_exchange`]) — the many hermetic test configs need no change.\n 971|    // [impl->REQ-HAZARD-BROKER-QUIC-DEADLINE]\n...\n 978|    /// (REQ-HAZARD-BROKER-QUIC-DEADLINE) — the B-half of REQ-HAZARD-PUMP-IPC-\n*979|    /// DEADLINE. A brain-waiting QUIC await against a black-holed/dead peer must\n 980|    /// fail PROMPTLY with an ORDINARY error the broker replies, never the\n 981|    /// unbounded await that wedged the single-threaded pump for 2.2h. On elapse\n 982|    /// the future is DROPPED — cancelling the in-flight connect / stream op, so\n...\n 1093|        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): the `connect` + `prove_membership`\n*1094|        // awaits are the exact unbounded sites a dead peer black-holes — wrap\n 1095|        // them so a non-responsive peer fails as an ordinary error within the bound.\n 1096|        self.bounded_block_on(async move {\n 1097|            // Q2 caveat: the blocking dial acquires a permit from the SAME\n...\n 1294|        match backing {\n*1295|            // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `open_bi` on a black-holed\n 1296|            // peer can block on stream flow-control credit the dead peer never grants.\n 1297|            Backing::Quic(conn) => self.bounded_block_on(async move {\n 1298|                let (send, recv) = conn\n...\n 1392|        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a\n*1393|        // black-holed QUIC stream blocks on flow-control the dead peer never grants.\n 1394|        // Loopback (in-process) never hangs, so the bound is a no-op for it.\n 1395|        self.bounded_block_on(async move {\n 1396|            let mut send = entry.send.lock().await;\n...\n 1852|\n*1853|        // A never-completing await (the black-holed-peer shape) fails at the bound.\n 1854|        let started = std::time::Instant::now();\n 1855|        let err = host\n 1856|            .bounded_block_on(std::future::pending::<io::Result<()>>())\n\n#### pump/\n##### mod.rs\n 530|    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n*531|    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n 532|    // hfenduleam stall). A timed-out read escalates to a supervised restart\n 533|    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n 534|    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n\n## spt/tests/\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...\n 281|/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads REAL\n*282|/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n 283|/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n 284|/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n 285|/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\n...\n 439|    //    post-resume instant, then measure a window. GREEN (Viewer re-attach): A_i kept\n*440|    //    control → its tally CLIMBS. RED (Control re-attach): resume become_controller'd the\n 441|    //    slot → A_i's sink was dropped → its tally FREEZES. The child keeps ticking either\n 442|    //    way (broker-hosted, independent of the brain), so a frozen tally is a displaced\n 443|    //    controller, not a dead child. ──\n...\n 491|             controller conn received {g} new output frames in the post-resume window (froze \\\n*492|             at {}). The respawn's resume_sessions STOLE the controller slot (Control \\\n 493|             re-attach → become_controller displaced the local controller) then never drained \\\n*494|             it — Failure A. GREEN (resume re-attaches Viewer) leaves every controller intact. \\\n 495|             ticks_before={t0:?} ticks_after={t1:?}\",\n 496|            sessions[i].0,\n 497|            t1[i],\n### trial_drain_drive_e2e.rs\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...\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 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...\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 142|/// BRAIN_WRITE_DEADLINE doc): a suspended brain conn with a stuck write on an idle session.\n 143|/// A CONTINUOUS flood (false_promote.rs) instead trips the 5s output-driven detach and only\n 144|/// works because that rig finishes < 5s — the slower subprocess rig would self-heal.\n...\n 258|\n*259|/// The broker's cumulative stall-evict tally via `KIND_STALL_EVICTS` (a PURE tally read —\n 260|/// it does NOT drive a reap, unlike a `KIND_SESSIONS` poll). `0` if unreachable/unreported.\n 261|fn read_evicts(name: &str) -> u64 {\n 262|    Brain::cold_start(name, 1)\n 263|        .ok()\n*264|        .and_then(|mut b| b.stall_evicts().ok().flatten())\n 265|        .map(|(count, _last_ms)| count)\n 266|        .unwrap_or(0)\n 267|}\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 493|    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\n*494|    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\n 495|    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\n 496|    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\n 497|    //    zero — the evict IS the reap. ──\n...\n 511|        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\n*512|         rollbacks={rollbacks:?} stall_evicts={evicts} \\\n 513|         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\n 514|    );\n 515|\n...\n 521|        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n*522|         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n 523|         the window. RED (comment the production `let _ = brain.sessions();` in \\\n 524|         brainproc.rs, rebuild): nothing drives the reap → the wedge strands the DRAINED \\\n 525|         gate → WindowElapsedAlive → rollback (the 2026-07-09 field freeze).\\n\\\n### whoami_identity_e2e.rs\n 8|//! context branch through exactly this seam (the 2026-07-15 hook-deadline\n*9|//! incident: 100+ git children, 29–35s, bodies black-holed).\n 10|//!\n 11|//! No daemon tree is spawned (pure CLI + on-disk perches) — LIGHT class by\n 12|//! the nextest.toml sweep criterion."}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt-daemon/src, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":127,"fileCount":20,"files":["crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/net_worker_starve.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/pumpdeadline.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt/tests/whoami_identity_e2e.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/attach.rs","count":2},{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":20},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":20},{"path":"crates/spt-daemon/tests/false_promote.rs","count":2},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":1},{"path":"crates/spt-daemon/tests/net_worker_starve.rs","count":3},{"path":"crates/spt-daemon/tests/netbroker.rs","count":4},{"path":"crates/spt-daemon/tests/pump.rs","count":4},{"path":"crates/spt-daemon/tests/pumpdeadline.rs","count":1},{"path":"crates/spt-daemon/src/attach.rs","count":1},{"path":"crates/spt-daemon/src/brain.rs","count":10},{"path":"crates/spt-daemon/src/brainproc.rs","count":3},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":2},{"path":"crates/spt-daemon/src/msg.rs","count":5},{"path":"crates/spt-daemon/src/nethost.rs","count":9},{"path":"crates/spt-daemon/src/pump/mod.rs","count":1},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":5},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":13},{"path":"crates/spt/tests/whoami_identity_e2e.rs","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/\n\n### tests/\n#### attach.rs\n 1204│//\n*1205│// [int->REQ-HAZARD-CONTROLLER-GAP-RESUME] The controller is driven through\n 1206│// `read_controller_event_resuming`, NOT raw `read_event`, because the b4\n 1207│// drop-don't-block fix decoupled controller backpressure from the drain: the\n 1208│// controller is now a NON-BLOCKING try_send that DROPS on a full channel\n     │...\n 1212│// surfaces to the strict controller cursor as an `output gap`. The helper\n*1213│// RESUMES-FROM-FLOOR on that gap (re-fetch from `controller_resume_floor`, B2 — a\n 1214│// re-fetch, not a viewer-style snap).\n 1215│//\n 1216│// VIEWER-ISOLATION asserts the wedged viewer does not STALL the controller — and a\n#### brain_decouple.rs\n  62│    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n *63│    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n  64│};\n  65│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n  66│use spt_daemon::Broker;\n    │...\n 195│/// Query the broker's stall-evict tally.\n*196│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197│    let name = name.to_string();\n 198│    let (tx, rx) = std::sync::mpsc::channel();\n 199│    thread::spawn(move || {\n 200│        let mut c = connect(&name);\n*201│        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 202│        loop {\n 203│            match read_frame(&mut c) {\n*204│                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 205│                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 206│                    let _ = tx.send(r.count);\n 207│                    return;\n    │...\n 221│#[test]\n*222│fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223│    init_home();\n 224│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225│    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n    │...\n 335│\n*336│    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n 337│\n 338│    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 339│    if let Some(pid) = broker.session_pid(sid) {\n    │...\n 344│        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n*345│         take_elapsed={took:?} stall_evicts={evicts} ===\"\n 346│    );\n 347│\n 348│    // The viewer kept ticking while the controller conn was wedged.\n    │...\n 383│// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\n*384│// connection: `stall_evict_controller` drops the `ControllerSink` (one\n 385│// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\n 386│// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\n 387│// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\n    │...\n 395│// This test reproduces that EXACT Windows failure mode with a real PTY child and\n*396│// a genuinely black-holed controller connection, then asserts the two things the\n 397│// logical gate cannot see:\n*398│//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\n 399│//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\n 400│//     client ever dropping its end;                                    ← RED pre-fix\n 401│//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\n    │...\n 416│/// `seq>0` controller frame; the burst floods a non-draining controller's socket\n*417│/// so its writer parks INSIDE `write_frame` — the exact black-hole the RCA pinned.\n 418│/// Sibling of [`flood_spawn_req`], but FINITE + input-triggered (the flood child\n 419│/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\n 420│#[cfg(windows)]\n    │...\n 442│/// One `KIND_SESSIONS` round-trip, returning the full reply. The dispatch runs\n*443│/// `reap_dead_controller` → the time-based `stall_evict_controller`, so polling\n 444│/// this both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 445│/// Bounded thread + channel (a wedge surfaces as a timeout, not a hang), like the\n*446│/// existing [`stall_evict_count`] helper. The transient conn is dropped when the\n 447│/// thread returns.\n 448│#[cfg(windows)]\n 449│fn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {\n    │...\n 473│#[test]\n*474│fn non_draining_controller_stall_evict_releases_writer_and_connection() {\n 475│    init_home();\n 476│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\n 477│    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n    │...\n 486│    // ── Step 2: spawn the real SEED+burst PTY child from conn A, then DROP A so the\n*487│    //    ONLY persistent brain conn later is the black-holed controller R. ──\n 488│    let mut a = connect(&name);\n 489│    send(\n 490│        &mut a,\n    │...\n 581│        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame is\n*582│        // proven delivered, then STOP reading forever (the black-hole). The remaining\n 583│        // ~1300 frames flood the now-unread socket → the R-writer wedges in write_frame.\n 584│        let post_deadline = Instant::now() + Duration::from_secs(5);\n 585│        let mut post = 0u32;\n    │...\n 597│        }\n*598│        // Hand the Stream back to main UNREAD (main holds it = the black-holed client).\n 599│        let _ = hand_tx.send((r, max_seq));\n 600│        // Thread exits; the broker's R-writer stays wedged on the remaining burst.\n 601│    });\n    │...\n 609│    let _ = go_tx.send(());\n*610│    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 611│    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n 612│    let mut r_stream: Option<Stream> = None;\n 613│    let mut max_drained_seq: u64 = 0;\n    │...\n 639│    // ONE wedged controller's release can be tallied by the LOGICAL reap\n*640│    // (stall_evict_controller), by the PHYSICAL bound (the conn watchdog poisons\n 641│    // first and note_controller_write_retired records the same event class —\n 642│    // the field-accepted v0.30.6 behavior), or by BOTH when the watchdog races\n 643│    // the reap's blocked-window sample. So the tally can step 0→2 and an\n    │...\n 646│    // real second eviction and still fails.\n*647│    let mut stall_evicts_seen: u64 = 0;\n 648│    let mut t_release: Option<Instant> = None;\n 649│    let mut frozen_cursor: Option<u64> = None;\n 650│    let step6_deadline = Instant::now() + Duration::from_secs(10);\n#### conn_blackhole_lifecycle.rs\n  *1│//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n   2│//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n   3│//! hosting REAL PTY children over the real local IPC.\n   4│//!\n    │...\n   9│//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n *10│//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n  11│//! finite burst floods its socket, so the broker's writer parks inside\n  12│//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n  13│//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n    │...\n  17│//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n *18│//!      receiving output throughout the black-hole incident (the fan-out never\n  19│//!      blocks the whole broker on the one wedged conn);\n  20│//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n  21│//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n    │...\n  25│//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n *26│//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n  27│//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n  28│//!      completion), and the viewer flows output AFTER the sample latches — proving\n  29│//!      the survivor is the viewer's handler, not a leaked one;\n    │...\n  45│//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n *46│//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n  47│//! Windows-mandatory physical-retirement leg for\n  48│//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n  49│//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n  50│//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n *51│//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n  52│//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n  53│//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n  54│//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n    │...\n  59│//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n *60│//! asserts, and the deterministic black-hole only reproduces on the Windows\n  61│//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n  62│//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n  63│//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n    │...\n  91│    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n *92│    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n *93│    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n  94│};\n  95│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n  96│use spt_daemon::Broker;\n    │...\n 136│/// session so its viewer has a continuous frame stream to keep ticking on\n*137│/// throughout the black-hole incident (invariant 1).\n 138│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139│    SpawnReq {\n 140│        program: \"cmd\".to_string(),\n    │...\n 159│/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n*160│/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163│    SpawnReq {\n    │...\n 197│/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199│    let name = name.to_string();\n 200│    let (tx, rx) = std::sync::mpsc::channel();\n 201│    thread::spawn(move || {\n 202│        let mut c = connect(&name);\n*203│        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 204│        loop {\n 205│            match read_frame(&mut c) {\n*206│                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 207│                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 208│                    let _ = tx.send(r.count);\n 209│                    return;\n    │...\n 222│/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223│/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224│/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n    │...\n 291│\n*292│    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293│    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294│    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295│    drop(a);\n 296│\n    │...\n 371│        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n*372│        // is proven delivered, then STOP reading forever (the black-hole). The\n 373│        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n 374│        let post_deadline = Instant::now() + Duration::from_secs(5);\n 375│        let mut post = 0u32;\n    │...\n 386│        }\n*387│        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388│        // client). The broker's R-writer stays wedged on the remaining burst.\n 389│        let _ = hand_tx.send((r, max_seq));\n 390│    });\n    │...\n 398│    let _ = go_tx.send(());\n*399│    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 400│    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n 401│    let mut r_stream: Option<Stream> = None;\n 402│    let mut max_drained_seq: u64 = 0;\n#### false_promote.rs\n 19│//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n*20│//! `write_frame` on a black-holed spawner conn. Round-4\n 21│//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n 22│//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n 23│//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n   │...\n 32│//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n*33│//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n 34│//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n 35│//! wall-clock only; it never relaxes an assertion).\n 36│//!\n#### inject_control_wedge.rs\n 522│        std::env::set_var(\"SPT_HOME\", dir.path());\n*523│        // W5-A: shrink the settle-gate timeout suite-wide. The mock PTY children here\n 524│        // (findstr/cat) never answer the DSR readiness probe, so the settle-gate always\n 525│        // elapses its bounded wait once per worker — its PRESENCE is the invariant, not\n 526│        // the length, so 80ms keeps the suite fast and removes the fixed-sleep timing\n#### net_worker_starve.rs\n  10│//! STAGE 1 (this file, first): the narrow mechanism probe. Stand a dialer whose\n *11│//! connects black-hole a dead peer, fire a `submit_dial` burst straight onto the\n  12│//! broker's own [`NetHost`] (the exact pump path), and read the two round-3\n  13│//! DIAGNOSTIC probes added to `nethost.rs`:\n  14│//!   - `active_dial_tasks()` — in-flight dead-peer connects on the 2 workers;\n    │...\n 106│/// THE DECIDER (round-3 ruling §5): a burst of `submit_dial`s to a peer that\n*107│/// accepts the QUIC handshake but black-holes the seed-proof. Reports whether\n 108│/// the burst holds the 2 workers (canary goes stale) or the awaits yield (canary\n 109│/// stays fresh). This test does not FAIL on either outcome — it PRINTS the verdict\n 110│/// the ruling needs; an explicit assert nails the direction once observed.\n    │...\n 121│\n*122│    // B: the black hole — accepts the handshake, never runs the proof responder.\n 123│    let broker_b = served_broker(&name_b, hermetic(Identity::generate()), &dir.path().join(\"b\"));\n 124│    let host_b = broker_b.net().expect(\"host b\");\n 125│    let b_addr = host_b.addr();\n#### netbroker.rs\n  53│/// is irrelevant; its PRESENCE is what makes the dialer open the stream and\n *54│/// block, giving the deterministic black hole the broker-side bound must cut.\n  55│fn proving(identity: Identity) -> NetConfig {\n  56│    let mut cfg = hermetic(identity);\n  57│    cfg.membership = Some(std::sync::Arc::new(|| {\n    │...\n 234│// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n*235│// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236│// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237│// error reply within the bound, NEVER an unbounded await. Exactly-once is\n 238│// preserved on timeout: the journaled dial records NO phantom conn — the journal\n    │...\n 260│\n*261│    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it\n 262│    // registers the conn) but never runs the proof responder, so A's\n 263│    // `prove_membership` awaits a control-stream reply that never comes.\n 264│    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));\n    │...\n 273│        .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))\n*274│        .expect_err(\"a black-holed dial must error, never block forever\");\n 275│    let elapsed = started.elapsed();\n 276│\n 277│    // The broker REPLIED an ordinary error within the bound — not a wedge, and\n#### pump.rs\n  67│/// dial runs `prove_membership`, which a `membership: None` peer never answers\n *68│/// (the documented mixed-pair deadlock) — the deterministic black hole the\n  69│/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its\n  70│/// PRESENCE is what makes the dialer open the proof control stream and block.\n  71│fn proving() -> NetConfig {\n    │...\n 505│\n*506│    // B: the black hole — accepts the handshake (`membership: None`) but never\n 507│    // runs the proof responder, so A's dial hangs in `prove_membership` until the\n 508│    // broker-side bound cuts it.\n 509│    let _b_broker = net_broker(&b_name, dir.path());\n    │...\n 645│/// hardcodes `hermetic()`; W2 needs a `proving()` LIVE peer alongside hermetic\n*646│/// black holes). Serve thread detached; effect log named per broker.\n 647│fn net_broker_with(name: &str, dir: &std::path::Path, cfg: NetConfig) -> Arc<Broker> {\n 648│    let host = NetHost::start(cfg).expect(\"net host start\");\n 649│    let broker = Broker::bind_in_with_net(name, dir.join(format!(\"{name}-eff.log\")), Some(host))\n    │...\n 712│\n*713│    // A's stores: the subnet whose roster is the live peer + the two black holes.\n 714│    let a_root = dir.path().join(\"a\");\n 715│    std::fs::create_dir_all(&a_root).unwrap();\n 716│    let mut a_subnets = SubnetStore::default();\n#### pumpdeadline.rs\n 10│//! The wedge this guards: the 2.2h hfenduleam pump stall on\n*11│//! `net_open_stream`'s unbounded `loop { read_event }` against a black-holed\n 12│//! peer the broker never answered for.\n 13│\n 14│use std::io;\n\n### src/\n#### attach.rs\n 151│/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\n*152│/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\n 153│/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\n 154│/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n 155│/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n#### brain.rs\n   45│    ViewerEvictedEvent,\n  *46│    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n   47│    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n   48│    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n   49│    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n     │...\n  194│/// **Why the split exists (REQ-HAZARD-PUMP-IPC-DEADLINE).** The pump is\n *195│/// single-threaded; a brain-IPC read that blocks forever on a black-holed peer\n  196│/// wedges the whole loop (the 2.2h hfenduleam stall). The deadline must be\n  197│/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n  198│/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n     │...\n  297│    /// brain-IPC read waits forever, which is correct everywhere EXCEPT the\n *298│    /// single-threaded pump, where a black-holed peer would wedge the whole loop\n  299│    /// (KNOWN-HAZARDS — a blocked brain-IPC read must never wedge the pump). The\n  300│    /// two facts are coupled at construction: `Some` ⇔ [`BrainConn::Split`].\n  301│    io_timeout: Option<Duration>,\n     │...\n  339│    /// every reply read is bounded ([`Brain::read_event_until`] via\n *340│    /// [`Brain::call_deadline`]) so a black-holed peer can never wedge the\n  341│    /// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a\n  342│    /// live `Whole` after the fact — moving the `Stream` out by value to split it\n  343│    /// is impossible behind `&mut self`), so a pump brain is split-and-bounded\n     │...\n 1155│    /// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): `(count,\n*1156│    /// last_ms)` — how many controllers were stall-evicted for a suspended/black-holed\n 1157│    /// brain conn since boot, and when the most recent fired. `None` when the broker\n 1158│    /// is too old to answer (an older broker replies `unknown command kind`), read as\n 1159│    /// \"not reported\" on `daemon status` — same forward-compat contract as\n     │...\n 1161│    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1162│    pub fn stall_evicts(&mut self) -> io::Result<Option<(u64, u64)>> {\n*1163│        self.send(KIND_STALL_EVICTS, serde_json::Value::Null)?;\n 1164│        loop {\n 1165│            let env = self.read_frame_until(None)?;\n 1166│            match env.kind.as_str() {\n*1167│                KIND_STALL_EVICTS_REPLY => {\n 1168│                    let ev: StallEvictsReply = serde_json::from_value(env.payload)\n 1169│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1170│                    return Ok(Some((ev.count, ev.last_ms)));\n     │...\n 1599│    /// does not close the conn), FREEZING the session immediately; a >15s\n*1600│    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n 1601│    /// (only an actively-producing stolen session blocks long enough to evict).\n 1602│    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n 1603│    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n     │...\n 1915│    /// KEYSTONE — the brain-level primitive the `serve_attach` controller\n*1916│    /// gap-resume (REQ-HAZARD-CONTROLLER-GAP-RESUME) is built on: a CONTROLLER on\n 1917│    /// the strict legacy reject-gap path (empty `session_cursors`) that FATALS a b4\n 1918│    /// drop-don't-block output-gap can RECOVER by re-subscribing from\n 1919│    /// [`Brain::controller_resume_floor`] (== `next_seq` == the gap error's `want`\n#### brainproc.rs\n 280│        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n*281│        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n 282│        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283│        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n 284│        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n    │...\n 287│        // drives the evict) stays false for the full trial → \"alive but never ready\"\n*288│        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n*289│        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n 290│        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291│        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292│        // net_status probe above already owns broker-liveness). Self-applies: it drives\n#### broker.rs\n   64│    KIND_ADAPTER_APPLY, KIND_APPLIED,\n  *65│    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n   66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n   67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n     │...\n  156│/// How long a controller's writer thread may sit BLOCKED inside a single socket\n *157│/// write — the brain subscriber suspended / black-holed — before a take/reattach\n  158│/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n  159│/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n  160│/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n     │...\n  254│/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n *255│/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n  256│/// line, not a silent absence.\n  257│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n *258│static STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\n *259│static STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n  260│\n  261│/// Record one stall-evict in the broker-global tally (best-effort observability).\n  262│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n *263│fn record_stall_evict() {\n *264│    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n *265│    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n  266│}\n  267│\n  268│/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n     │...\n  271│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n *272│pub(crate) fn stall_evict_stats() -> (u64, u64) {\n  273│    (\n *274│        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n *275│        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n  276│    )\n  277│}\n  278│\n     │...\n  515│    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n *516│    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n  517│    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n  518│    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n  519│    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n     │...\n  577│    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**\n *578│    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).\n  579│    ///\n  580│    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**\n  581│    /// (never decreases) — see [`advanced_cursor`]. Both invariants are\n     │...\n  700│    /// --attach` case #6 is about (attach to an already-producing TUI). A\n *701│    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n  702│    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n  703│    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n  704│    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n     │...\n  729│    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n *730│    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n  731│    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n  732│    /// (bounded wedge preserved). The writer still does the blocking socket write +\n  733│    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n     │...\n 1008│        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n*1009│        self.stall_evict_controller(Instant::now(), StampMode::Deferred)\n 1010│    }\n 1011│\n 1012│    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and\n     │...\n 1023│    /// socket write for at least `deadline` (the brain-subscriber conn suspended /\n*1024│    /// black-holed). An IDLE controller (writer parked on `rx.recv()`, nothing to\n 1025│    /// write) reads NOT stalled: its [`ControllerSink::write_blocked_since`] is `None`.\n 1026│    /// Pure over the injected `now` + `deadline` so the unit tests it with no thread.\n 1027│    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n     │...\n 1040│    /// it (REQ-UPDATE-PROMOTE-DRAINED). A killed OLD brain generation whose local\n*1041│    /// controller conn is black-holed reads wedged here until the W2 stall-evict\n 1042│    /// releases it (or the conn EOFs); a cleanly-closed conn is simply absent. A\n 1043│    /// REMOTE operator's wedged conn (`by: Some`) is the W2 stall-evict concern, NOT\n 1044│    /// an old-gen brain conn, so it is EXCLUDED — a stuck remote viewer must never\n     │...\n 1063│    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1064│    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n 1065│        let deadline = brain_write_deadline();\n 1066│        if !self.controller_write_stalled(now, deadline) {\n 1067│            return false;\n     │...\n 1072│        eprintln!(\n*1073│            \"BRAIN_SUBSCRIBER_STALL_EVICT:{} conn={conn} {} — controller writer blocked > {}ms \\\n#### conn.rs\n  11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n *12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n  13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n  14│//! connection, so the writer (and every write queued behind the same gate)\n  15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n    │...\n 780│        // Poisoned + retired: the next write refuses immediately.\n*781│        let refused = conn.write(&env).expect_err(\"poisoned conn refuses writes\");\n 782│        assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);\n 783│    }\n 784│\n#### msg.rs\n 103│/// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — how many\n*104│/// controllers were stall-evicted for a suspended/black-holed brain conn since\n 105│/// boot, and when the most recent fired. Surfaced on `daemon status` so a brain\n 106│/// that never returns is a diagnosable fact. An older broker predating this kind\n 107│/// replies `unknown command kind` (forward-compat), read as \"not reported\".\n*108│pub const KIND_STALL_EVICTS: &str = \"stall-evicts\";\n 109│/// Broker→brain: the stall-evict tally reply.\n*110│pub const KIND_STALL_EVICTS_REPLY: &str = \"stall-evicts-reply\";\n 111│/// Brain→broker: dial a peer over the broker-owned endpoint (D4a). With an\n 112│/// `op_id` the dial is exactly-once (journaled like an idempotent `input`).\n 113│pub const KIND_NET_DIAL: &str = \"net-dial\";\n    │...\n 345│    /// handshake-proven origin), `None` = local (the daemon brain's own\n*346│    /// spawn-reader / resume). The broker keys controller identity on this:\n 347│    /// a same-`by` re-subscribe silently re-takes the controller (no\n 348│    /// `Displaced`); a `None` (local) re-attach NEVER displaces a remote\n 349│    /// controller (it falls back to viewer) — the gate-#7 invariant.\n    │...\n 557│pub struct StallEvictsReply {\n*558│    /// Controllers stall-evicted for a suspended/black-holed brain conn since broker boot.\n 559│    #[serde(default)]\n 560│    pub count: u64,\n 561│    /// Wall-clock epoch-ms of the most recent stall-evict (0 = none since boot).\n#### nethost.rs\n   59│/// brain-waiting QUIC await — `dial` / `open_stream` / `send_stream` — is bounded\n  *60│/// by this so a black-holed or dead peer fails PROMPTLY with an ORDINARY error\n   61│/// the broker replies, never an unbounded await that wedges the single-threaded\n   62│/// pump (the B-half of REQ-HAZARD-PUMP-IPC-DEADLINE; the 2.2h hfenduleam wedge).\n   63│/// Chosen at **10s**: comfortably above any legitimate LAN/relay connect +\n     │...\n   70│/// frame's IPC transit + the brain's read scheduling. Test-overridable via\n  *71│/// [`NetHost::set_quic_op_timeout`] so the hermetic black-hole tests fail fast.\n   72│const BROKER_QUIC_OP_TIMEOUT_MS: u64 = 10_000;\n   73│\n   74│/// Bounded concurrent-dial cap (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G4): a\n     │...\n  759│    /// The broker-side QUIC-op deadline in ms (REQ-HAZARD-BROKER-QUIC-DEADLINE),\n *760│    /// default [`BROKER_QUIC_OP_TIMEOUT_MS`]; the hermetic black-hole tests lower\n  761│    /// it via [`Self::set_quic_op_timeout`]. Interior-mutable so the setter takes\n  762│    /// `&self`, like the live host's other post-`start` knobs (`set_roster_exchange`).\n  763│    quic_op_timeout_ms: AtomicU64,\n     │...\n  967│    /// Production keeps the [`BROKER_QUIC_OP_TIMEOUT_MS`] default; the hermetic\n *968│    /// black-hole tests set a short bound so a dead-peer await fails fast instead\n  969│    /// of pinning the test for ~10s. Off [`NetConfig`] by design (mirrors\n  970│    /// [`Self::set_roster_exchange`]) — the many hermetic test configs need no change.\n  971│    // [impl->REQ-HAZARD-BROKER-QUIC-DEADLINE]\n     │...\n  978│    /// (REQ-HAZARD-BROKER-QUIC-DEADLINE) — the B-half of REQ-HAZARD-PUMP-IPC-\n *979│    /// DEADLINE. A brain-waiting QUIC await against a black-holed/dead peer must\n  980│    /// fail PROMPTLY with an ORDINARY error the broker replies, never the\n  981│    /// unbounded await that wedged the single-threaded pump for 2.2h. On elapse\n  982│    /// the future is DROPPED — cancelling the in-flight connect / stream op, so\n     │...\n 1093│        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): the `connect` + `prove_membership`\n*1094│        // awaits are the exact unbounded sites a dead peer black-holes — wrap\n 1095│        // them so a non-responsive peer fails as an ordinary error within the bound.\n 1096│        self.bounded_block_on(async move {\n 1097│            // Q2 caveat: the blocking dial acquires a permit from the SAME\n     │...\n 1294│        match backing {\n*1295│            // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `open_bi` on a black-holed\n 1296│            // peer can block on stream flow-control credit the dead peer never grants.\n 1297│            Backing::Quic(conn) => self.bounded_block_on(async move {\n 1298│                let (send, recv) = conn\n     │...\n 1392│        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a\n*1393│        // black-holed QUIC stream blocks on flow-control the dead peer never grants.\n 1394│        // Loopback (in-process) never hangs, so the bound is a no-op for it.\n 1395│        self.bounded_block_on(async move {\n 1396│            let mut send = entry.send.lock().await;\n     │...\n 1852│\n*1853│        // A never-completing await (the black-holed-peer shape) fails at the bound.\n 1854│        let started = std::time::Instant::now();\n 1855│        let err = host\n 1856│            .bounded_block_on(std::future::pending::<io::Result<()>>())\n\n#### pump/\n##### mod.rs\n 530│    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n*531│    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n 532│    // hfenduleam stall). A timed-out read escalates to a supervised restart\n 533│    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n 534│    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n\n## spt/tests/\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    │...\n 281│/// The rig's [`TrialEnv`]: a scripted `AppliedPending` trial whose DRAINED gate reads REAL\n*282│/// broker truth off the live in-proc broker. There is NO black-holed controller here (every\n 283│/// `A_i` is a healthy reader), so `old_gen_drained()` is true and the candidate promotes as\n 284│/// soon as it stamps ready — the promote is the no-regression guard (assertion 3), while the\n 285│/// RED→GREEN discriminator is the out-of-band tick-continuity check on the `A_i` conns.\n    │...\n 439│    //    post-resume instant, then measure a window. GREEN (Viewer re-attach): A_i kept\n*440│    //    control → its tally CLIMBS. RED (Control re-attach): resume become_controller'd the\n 441│    //    slot → A_i's sink was dropped → its tally FREEZES. The child keeps ticking either\n 442│    //    way (broker-hosted, independent of the brain), so a frozen tally is a displaced\n 443│    //    controller, not a dead child. ──\n    │...\n 491│             controller conn received {g} new output frames in the post-resume window (froze \\\n*492│             at {}). The respawn's resume_sessions STOLE the controller slot (Control \\\n 493│             re-attach → become_controller displaced the local controller) then never drained \\\n*494│             it — Failure A. GREEN (resume re-attaches Viewer) leaves every controller intact. \\\n 495│             ticks_before={t0:?} ticks_after={t1:?}\",\n 496│            sessions[i].0,\n 497│            t1[i],\n### trial_drain_drive_e2e.rs\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    │...\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  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    │...\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 142│/// BRAIN_WRITE_DEADLINE doc): a suspended brain conn with a stuck write on an idle session.\n 143│/// A CONTINUOUS flood (false_promote.rs) instead trips the 5s output-driven detach and only\n 144│/// works because that rig finishes < 5s — the slower subprocess rig would self-heal.\n    │...\n 258│\n*259│/// The broker's cumulative stall-evict tally via `KIND_STALL_EVICTS` (a PURE tally read —\n 260│/// it does NOT drive a reap, unlike a `KIND_SESSIONS` poll). `0` if unreachable/unreported.\n 261│fn read_evicts(name: &str) -> u64 {\n 262│    Brain::cold_start(name, 1)\n 263│        .ok()\n*264│        .and_then(|mut b| b.stall_evicts().ok().flatten())\n 265│        .map(|(count, _last_ms)| count)\n 266│        .unwrap_or(0)\n 267│}\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 493│    // ── Leg 2 (doyle FINALIZE): the stall-evict tally proves a reap FIRED via the\n*494│    //    brain's own drive. KIND_STALL_EVICTS is a pure tally read — it does NOT reap.\n 495│    //    The test itself NEVER calls sessions(), and livehost's poll is silenced, so the\n 496│    //    subprocess candidate's heartbeat is the ONLY possible driver. NOT asserting\n 497│    //    zero — the evict IS the reap. ──\n    │...\n 511│        \"=== UPDATE-WEDGE A' GATE: promoted={promoted} promotions={promotions:?} \\\n*512│         rollbacks={rollbacks:?} stall_evicts={evicts} \\\n 513│         (GREEN = promoted + evicts>=1; RED with brain.sessions() commented = rolled back) ===\"\n 514│    );\n 515│\n    │...\n 521│        \"the trial candidate must PROMOTE: its own heartbeat Brain::sessions() drive \\\n*522│         must reap the black-holed old-gen controller so old_gen_drained() flips within \\\n 523│         the window. RED (comment the production `let _ = brain.sessions();` in \\\n 524│         brainproc.rs, rebuild): nothing drives the reap → the wedge strands the DRAINED \\\n 525│         gate → WindowElapsedAlive → rollback (the 2026-07-09 field freeze).\\n\\\n### whoami_identity_e2e.rs\n  8│//! context branch through exactly this seam (the 2026-07-15 hook-deadline\n *9│//! incident: 100+ git children, 29–35s, bodies black-holed).\n 10│//!\n 11│//! No daemon tree is spawned (pure CLI + on-disk perches) — LIGHT class by\n 12│//! the nextest.toml sweep criterion."},"isError":false,"timestamp":1784195850533}}
{"type":"message","id":"c71f1e31","parentId":"651c6b95","timestamp":"2026-07-16T09:57:30.536Z","message":{"role":"toolResult","toolCallId":"call_FxEksntciaPYsBPLCt7tPVm9|fc_0698befac979932d016a58ab089e98819abab516c415ae4731","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/\n\n### tests/\n#### endpoint_autostart_e2e.rs\n 8|//! runs on a boot background thread (`replay_startup_endpoints`, after broker\n*9|//! bind) so a restart brings the endpoint back up on its own — a FRESH session\n 10|//! (shape (a): a startup default, not a session restore).\n 11|//!\n 12|//! This gate proves the FIX end to end by driving the REAL `spt` binary across a\n...\n 287|\n*288|    // ── (6) Daemon B — the RESTART. NO `endpoint run`. Its boot replay thread\n 289|    //    (after broker bind) must bring `gwauto` back up on its own. ──\n 290|    let daemon_b_log = home.path().join(\"daemon-b.stderr.log\");\n 291|    let log_b = std::fs::File::create(&daemon_b_log).expect(\"create daemon B log\");\n...\n 390|    );\n*391|    // (b) THE KEYSTONE: the restart's boot replay brought the saved endpoint back\n 392|    //     up on its own — the loud ENDPOINT_AUTOSTART:<id> token is present in\n 393|    //     daemon B's stderr, emitted only on a successful launch_harness_brokered_in.\n 394|    let line = match autostart_line {\n#### live_adapt_translation_swap_e2e.rs\n 108|#[test]\n*109|fn adapter_apply_swaps_locked_translation_binary_without_restarting_endpoint() {\n 110|    // Serialize against the sibling tests — shared process-global SPT_HOME.\n 111|    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\n 112|    let now_ms = || {\n\n### src/\n#### cli.rs\n 251|        /// The full-cycle form: fetch, update adapters, then finish by\n*252|        /// restarting the daemon onto the new version (`update apply --finish`)\n 253|        /// as the final step — so the whole node, coordinator and live agents,\n 254|        /// runs the new version when it returns. The restart bounces hosted\n 255|        /// sessions (they come back automatically).\n...\n 1454|            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n*1455|            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n 1456|            None => cmd_update_composite(core_only, restart),\n 1457|        },\n 1458|        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n...\n 4818|        \"Installed spt-core {ver} on disk. The daemon is offline, so nothing is running the new \\\n*4819|         version yet — run `spt update apply --finish` to bring it up on the new version now (any \\\n 4820|         hosted sessions come back automatically), or `spt daemon start`.\\nChangelog: {RELEASES_URL}\"\n 4821|    )\n 4822|}\n...\n 4958|/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\n*4959|/// REQ-UPDATE-RESTART-SAFE-SWAP).\n 4960|#[derive(Debug, PartialEq, Eq, Clone, Copy)]\n 4961|enum UpdateLeg {\n 4962|    /// `update fetch --apply` — stage + install the core in one step (the\n...\n 4968|    Adapters,\n*4969|    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\n 4970|    /// spt-hosted session the invoker's PTY dies here, so this leg is always\n 4971|    /// planned LAST (everything else has completed by then).\n 4972|    Finish,\n...\n 4981|// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n*4982|// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n 4983|fn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\n 4984|    let mut legs = Vec::new();\n 4985|    legs.push(if restart {\n...\n 5003|/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n*5004|/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n*5005|/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n 5006|///\n 5007|/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\n 5008|/// ABORTS the remaining legs — updating adapters against a channel that just\n...\n 5015|// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n*5016|// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n 5017|fn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n 5018|    let mut worst = 0;\n 5019|    for leg in plan_update_legs(core_only, restart) {\n...\n 5139|\n*5140|/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\n 5141|/// coordinator running new-agent code), then RESTART the daemon onto them so the\n 5142|/// whole node runs the new version in one command. The restart rides\n*5143|/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\n 5144|/// online hosted endpoints, so the cycle is not a massacre.\n 5145|// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n 5146|fn cmd_update_apply_finish(\n...\n 6919|            \"\\n  input-translation: FAILED ({why}) — typed input may not reach \\\n*6920|             this session; restart it with `spt endpoint run`\"\n 6921|        ));\n 6922|    }\n 6923|    pin\n...\n 10659|            // translation binary that locks the install dir → hand the apply to the\n*10660|            // daemon (stop→swap→restart). No live endpoint → swap directly.\n 10661|            let applied = if adapter_has_live_endpoint(&record.name) {\n 10662|                eprintln!(\n 10663|                    \"ADAPTER_UPDATE_LIVE:{}: live endpoint(s) — daemon-coordinated apply\",\n...\n 12718|    // ── THE-FORKENING W4: the update-UX cluster (REQ-UPDATE-ADAPTERS-VERB,\n*12719|    // REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-RESTART-SAFE-SWAP,\n 12720|    // REQ-DAEMON-REFRESH). ──\n 12721|\n 12722|    // [unit->REQ-UPDATE-ADAPTERS-VERB] comma-list parsing shared by both verb\n...\n 12893|\n*12894|    // [unit->REQ-UPDATE-RESTART-SAFE-SWAP] lethal-leg-LAST: `--restart` plans\n 12895|    // fetch (stage only, no in-place apply) → adapters → finish, with the\n 12896|    // whole-daemon restart strictly FINAL in every shape so all other legs\n 12897|    // complete from any invoking context; `-c` composes (adapters dropped,\n...\n 17752|            assert!(\n*17753|                msg.contains(\"spt update apply --finish\"),\n 17754|                \"points at the one-shot finish: {msg}\"\n 17755|            );\n 17756|            assert!(\n\n## spt-daemon/\n\n### tests/\n#### daemon_e2e.rs\n 39|// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n*40|// brain-restart survival, not the process-level swap. Process-level endpoint\n 41|// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n 42|// [int->REQ-DAEMON-4]\n 43|// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n#### endpoint_survival.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n 2|//! survival, against a REAL broker over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n...\n 8|//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n*9|//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n 10|//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n 11|//! endpoint the restart orphaned, resuming from its last ledger session.\n 12|//!\n...\n 84|\n*85|// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 86|#[test]\n*87|fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n 88|    let home = tempfile::TempDir::new().expect(\"temp home\");\n 89|    std::env::set_var(\"SPT_HOME\", home.path());\n 90|\n...\n 121|    let registered = spt_runtime::registry::registered(&adapters_dir);\n*122|    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 123|\n 124|    // The endpoint's harness was re-run → a live broker session now exists for it.\n 125|    let mut appeared = false;\n...\n 143|        appeared,\n*144|        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\n 145|         (a live broker session appears for it). RED without the pass: no session.\"\n 146|    );\n 147|}\n\n### src/\n#### livehost.rs\n 417|/// action behind BOTH the wake-resume leg ([`resume_woken_endpoint`]) and the\n*418|/// daemon-restart survival pass ([`resume_restart_orphaned_endpoints`]). `reason_tag`\n 419|/// names the trigger in the log breadcrumb (`WAKE` / `DAEMON_RESTART`), scoped so an\n 420|/// operator can tell a woken resume from a restart-survival re-run. Records the custody\n 421|/// guard pid + clears any stale `host_error` on success; stamps `host_error` on\n...\n 423|/// stays daemon-authoritative, the D9-2 posture the resume leg already keeps).\n*424|// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 425|#[allow(clippy::too_many_arguments)] // resume material + the two registry lookups; a struct would obscure the call\n 426|fn launch_ledger_resume(\n 427|    perch: &Path,\n...\n 482|\n*483|/// The daemon-restart re-run decision (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL). A pure\n 484|/// function over a candidate perch's facts, so the full belt matrix is unit-tested\n 485|/// without a broker. Only the daemon-restart-ORPHANED, spt-hosted, DEAD set re-runs:\n 486|/// - `state != live_agent` → Skip (not a live agent).\n...\n 498|///   a live custody process; refuse loudly, the breadcrumb feeds the W4 dup-wake rig).\n*499|// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 500|#[derive(Debug, PartialEq, Eq)]\n 501|enum RestartResume {\n 502|    Resume,\n...\n 506|\n*507|// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 508|fn restart_resume_gate(\n 509|    state: &str,\n 510|    status: Option<&str>,\n...\n 536|\n*537|/// LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): on daemon start, RE-RUN the\n 538|/// harness of every previously-ONLINE spt-hosted endpoint this daemon (re)start\n 539|/// orphaned — so a `daemon stop`+`start` (the apply notice's OWN instruction) no longer\n 540|/// massacres them (seed #6, the field-accept blocker). Runs ONCE at\n...\n 548|/// hiccup — the safe direction, same posture as [`reconcile_hosted_liveness`]).\n*549|// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n*550|pub fn resume_restart_orphaned_endpoints(\n 551|    owlery: &Path,\n 552|    registered: &[(AdapterRecord, Manifest)],\n 553|    adapters_dir: &Path,\n...\n 995|            gc_live_bin_dirs(&owlery);\n*996|            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n*997|            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n 998|            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n 999|            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n 1000|            // online\" signal). A brain restart with a surviving broker re-runs nothing\n 1001|            // (its sessions are live, not orphaned). Belts inside the gate.\n*1002|            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n*1003|            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 1004|        }\n 1005|        while !stop.load(Ordering::SeqCst) {\n 1006|            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n...\n 2095|\n*2096|    // [unit->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL] the daemon-restart re-run GATE + both\n 2097|    // belts, as a pure matrix. Only a previously-ONLINE, spt-hosted (controllable),\n 2098|    // orphaned (no live session), non-relay, DEAD-custody endpoint re-runs; every other\n 2099|    // class is excluded, and a LIVE custody pid refuses (the dup guard, not a silent skip)."}],"details":{"scopePath":"crates/spt/tests, crates/spt-daemon/tests, crates/spt/src/cli.rs, crates/spt-daemon/src/livehost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":40,"fileCount":6,"files":["crates/spt/tests/endpoint_autostart_e2e.rs","crates/spt/tests/live_adapt_translation_swap_e2e.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt/src/cli.rs","crates/spt-daemon/src/livehost.rs"],"fileMatches":[{"path":"crates/spt/tests/endpoint_autostart_e2e.rs","count":3},{"path":"crates/spt/tests/live_adapt_translation_swap_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":6},{"path":"crates/spt/src/cli.rs","count":16},{"path":"crates/spt-daemon/src/livehost.rs","count":13}],"truncated":false,"displayContent":"# crates/\n\n## spt/\n\n### tests/\n#### endpoint_autostart_e2e.rs\n   8│//! runs on a boot background thread (`replay_startup_endpoints`, after broker\n  *9│//! bind) so a restart brings the endpoint back up on its own — a FRESH session\n  10│//! (shape (a): a startup default, not a session restore).\n  11│//!\n  12│//! This gate proves the FIX end to end by driving the REAL `spt` binary across a\n    │...\n 287│\n*288│    // ── (6) Daemon B — the RESTART. NO `endpoint run`. Its boot replay thread\n 289│    //    (after broker bind) must bring `gwauto` back up on its own. ──\n 290│    let daemon_b_log = home.path().join(\"daemon-b.stderr.log\");\n 291│    let log_b = std::fs::File::create(&daemon_b_log).expect(\"create daemon B log\");\n    │...\n 390│    );\n*391│    // (b) THE KEYSTONE: the restart's boot replay brought the saved endpoint back\n 392│    //     up on its own — the loud ENDPOINT_AUTOSTART:<id> token is present in\n 393│    //     daemon B's stderr, emitted only on a successful launch_harness_brokered_in.\n 394│    let line = match autostart_line {\n#### live_adapt_translation_swap_e2e.rs\n 108│#[test]\n*109│fn adapter_apply_swaps_locked_translation_binary_without_restarting_endpoint() {\n 110│    // Serialize against the sibling tests — shared process-global SPT_HOME.\n 111│    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\n 112│    let now_ms = || {\n\n### src/\n#### cli.rs\n   251│        /// The full-cycle form: fetch, update adapters, then finish by\n  *252│        /// restarting the daemon onto the new version (`update apply --finish`)\n   253│        /// as the final step — so the whole node, coordinator and live agents,\n   254│        /// runs the new version when it returns. The restart bounces hosted\n   255│        /// sessions (they come back automatically).\n      │...\n  1454│            // [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n *1455│            // [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n  1456│            None => cmd_update_composite(core_only, restart),\n  1457│        },\n  1458│        // [impl->REQ-INSTALL-BOOTSTRAP-VERB]\n      │...\n  4818│        \"Installed spt-core {ver} on disk. The daemon is offline, so nothing is running the new \\\n *4819│         version yet — run `spt update apply --finish` to bring it up on the new version now (any \\\n  4820│         hosted sessions come back automatically), or `spt daemon start`.\\nChangelog: {RELEASES_URL}\"\n  4821│    )\n  4822│}\n      │...\n  4958│/// One leg of the bare `spt update` composite (REQ-UPDATE-DEFAULT-COMPOSITE,\n *4959│/// REQ-UPDATE-RESTART-SAFE-SWAP).\n  4960│#[derive(Debug, PartialEq, Eq, Clone, Copy)]\n  4961│enum UpdateLeg {\n  4962│    /// `update fetch --apply` — stage + install the core in one step (the\n      │...\n  4968│    Adapters,\n *4969│    /// `update apply --finish` — swap + full daemon restart. LETHAL: from an\n  4970│    /// spt-hosted session the invoker's PTY dies here, so this leg is always\n  4971│    /// planned LAST (everything else has completed by then).\n  4972│    Finish,\n      │...\n  4981│// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n *4982│// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n  4983│fn plan_update_legs(core_only: bool, restart: bool) -> Vec<UpdateLeg> {\n  4984│    let mut legs = Vec::new();\n  4985│    legs.push(if restart {\n      │...\n  5003│/// PTY keep running (applyhost::apply_staged) — so no re-run machinery exists.\n *5004│/// With `--restart` (REQ-UPDATE-RESTART-SAFE-SWAP) the plan is fetch → adapters\n *5005│/// → `apply --finish` LAST: the one-step safe full-cycle swap.\n  5006│///\n  5007│/// Failure policy: a CORE-leg failure (fetch/apply refused, gh missing…)\n  5008│/// ABORTS the remaining legs — updating adapters against a channel that just\n      │...\n  5015│// [impl->REQ-UPDATE-DEFAULT-COMPOSITE]\n *5016│// [impl->REQ-UPDATE-RESTART-SAFE-SWAP]\n  5017│fn cmd_update_composite(core_only: bool, restart: bool) -> i32 {\n  5018│    let mut worst = 0;\n  5019│    for leg in plan_update_legs(core_only, restart) {\n      │...\n  5139│\n *5140│/// `spt update apply --finish` — swap the bytes daemonless (never leaving an old\n  5141│/// coordinator running new-agent code), then RESTART the daemon onto them so the\n  5142│/// whole node runs the new version in one command. The restart rides\n *5143│/// REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL: `daemon start` re-runs the previously\n  5144│/// online hosted endpoints, so the cycle is not a massacre.\n  5145│// [impl->REQ-UPDATE-ONE-SHOT-FINISH]\n  5146│fn cmd_update_apply_finish(\n      │...\n  6919│            \"\\n  input-translation: FAILED ({why}) — typed input may not reach \\\n *6920│             this session; restart it with `spt endpoint run`\"\n  6921│        ));\n  6922│    }\n  6923│    pin\n      │...\n 10659│            // translation binary that locks the install dir → hand the apply to the\n*10660│            // daemon (stop→swap→restart). No live endpoint → swap directly.\n 10661│            let applied = if adapter_has_live_endpoint(&record.name) {\n 10662│                eprintln!(\n 10663│                    \"ADAPTER_UPDATE_LIVE:{}: live endpoint(s) — daemon-coordinated apply\",\n      │...\n 12718│    // ── THE-FORKENING W4: the update-UX cluster (REQ-UPDATE-ADAPTERS-VERB,\n*12719│    // REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-RESTART-SAFE-SWAP,\n 12720│    // REQ-DAEMON-REFRESH). ──\n 12721│\n 12722│    // [unit->REQ-UPDATE-ADAPTERS-VERB] comma-list parsing shared by both verb\n      │...\n 12893│\n*12894│    // [unit->REQ-UPDATE-RESTART-SAFE-SWAP] lethal-leg-LAST: `--restart` plans\n 12895│    // fetch (stage only, no in-place apply) → adapters → finish, with the\n 12896│    // whole-daemon restart strictly FINAL in every shape so all other legs\n 12897│    // complete from any invoking context; `-c` composes (adapters dropped,\n      │...\n 17752│            assert!(\n*17753│                msg.contains(\"spt update apply --finish\"),\n 17754│                \"points at the one-shot finish: {msg}\"\n 17755│            );\n 17756│            assert!(\n\n## spt-daemon/\n\n### tests/\n#### daemon_e2e.rs\n 39│// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n*40│// brain-restart survival, not the process-level swap. Process-level endpoint\n 41│// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n 42│// [int->REQ-DAEMON-4]\n 43│// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n#### endpoint_survival.rs\n  *1│//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n   2│//! survival, against a REAL broker over the real local IPC.\n   3│//!\n   4│//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n    │...\n   8│//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n  *9│//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n  10│//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n  11│//! endpoint the restart orphaned, resuming from its last ledger session.\n  12│//!\n    │...\n  84│\n *85│// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n  86│#[test]\n *87│fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n  88│    let home = tempfile::TempDir::new().expect(\"temp home\");\n  89│    std::env::set_var(\"SPT_HOME\", home.path());\n  90│\n    │...\n 121│    let registered = spt_runtime::registry::registered(&adapters_dir);\n*122│    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 123│\n 124│    // The endpoint's harness was re-run → a live broker session now exists for it.\n 125│    let mut appeared = false;\n    │...\n 143│        appeared,\n*144│        \"the daemon-restart survival pass must re-run the orphaned endpoint's harness \\\n 145│         (a live broker session appears for it). RED without the pass: no session.\"\n 146│    );\n 147│}\n\n### src/\n#### livehost.rs\n  417│/// action behind BOTH the wake-resume leg ([`resume_woken_endpoint`]) and the\n *418│/// daemon-restart survival pass ([`resume_restart_orphaned_endpoints`]). `reason_tag`\n  419│/// names the trigger in the log breadcrumb (`WAKE` / `DAEMON_RESTART`), scoped so an\n  420│/// operator can tell a woken resume from a restart-survival re-run. Records the custody\n  421│/// guard pid + clears any stale `host_error` on success; stamps `host_error` on\n     │...\n  423│/// stays daemon-authoritative, the D9-2 posture the resume leg already keeps).\n *424│// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n  425│#[allow(clippy::too_many_arguments)] // resume material + the two registry lookups; a struct would obscure the call\n  426│fn launch_ledger_resume(\n  427│    perch: &Path,\n     │...\n  482│\n *483│/// The daemon-restart re-run decision (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL). A pure\n  484│/// function over a candidate perch's facts, so the full belt matrix is unit-tested\n  485│/// without a broker. Only the daemon-restart-ORPHANED, spt-hosted, DEAD set re-runs:\n  486│/// - `state != live_agent` → Skip (not a live agent).\n     │...\n  498│///   a live custody process; refuse loudly, the breadcrumb feeds the W4 dup-wake rig).\n *499│// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n  500│#[derive(Debug, PartialEq, Eq)]\n  501│enum RestartResume {\n  502│    Resume,\n     │...\n  506│\n *507│// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n  508│fn restart_resume_gate(\n  509│    state: &str,\n  510│    status: Option<&str>,\n     │...\n  536│\n *537│/// LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): on daemon start, RE-RUN the\n  538│/// harness of every previously-ONLINE spt-hosted endpoint this daemon (re)start\n  539│/// orphaned — so a `daemon stop`+`start` (the apply notice's OWN instruction) no longer\n  540│/// massacres them (seed #6, the field-accept blocker). Runs ONCE at\n     │...\n  548│/// hiccup — the safe direction, same posture as [`reconcile_hosted_liveness`]).\n *549│// [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n *550│pub fn resume_restart_orphaned_endpoints(\n  551│    owlery: &Path,\n  552│    registered: &[(AdapterRecord, Manifest)],\n  553│    adapters_dir: &Path,\n     │...\n  995│            gc_live_bin_dirs(&owlery);\n *996│            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n *997│            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n  998│            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n  999│            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n 1000│            // online\" signal). A brain restart with a surviving broker re-runs nothing\n 1001│            // (its sessions are live, not orphaned). Belts inside the gate.\n*1002│            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n*1003│            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 1004│        }\n 1005│        while !stop.load(Ordering::SeqCst) {\n 1006│            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n     │...\n 2095│\n*2096│    // [unit->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL] the daemon-restart re-run GATE + both\n 2097│    // belts, as a pure matrix. Only a previously-ONLINE, spt-hosted (controllable),\n 2098│    // orphaned (no live session), non-relay, DEAD-custody endpoint re-runs; every other\n 2099│    // class is excluded, and a LIVE custody pid refuses (the dup guard, not a silent skip)."},"isError":false,"timestamp":1784195850536}}
{"type":"message","id":"3c1ceea2","parentId":"c71f1e31","timestamp":"2026-07-16T09:57:49.648Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Diagnosing serve_attach thread discrepancies**\n\n**Tracing dispatcher thread behavior**"},{"type":"toolCall","id":"call_OGvVXD0WEZdBccfk90udttzr|fc_0698befac979932d016a58ab1d1bc8819a970eee858c219ed5","name":"grep","arguments":{"i":"Tracing attach dispatcher ownership","pattern":"serve_attach\\(|spawn_inbound|run_inbound|dispatch_inbound|KIND_NET_STREAM|NetStreamOpened|AttachRecord::Request|spawn_net_consumers","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_XvN7ZG6TEum29Cn1U1hmHL0P|fc_0698befac979932d016a58ab1d1bdc819ab380355b3db47c05","name":"grep","arguments":{"i":"Finding stream lifecycle on conn drop","pattern":"remove.*stream|finish_stream|NetStreamEof|net_stream.*finish|stream.*closed|close.*stream|drop.*stream|unsubscribe.*stream","path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_4j4OX07YHY0bh5sxkQSoNUbp|fc_0698befac979932d016a58ab1d1be4819abdc4200c61358bbd","name":"grep","arguments":{"i":"Finding rc liveness and reconnect trigger","pattern":"attach_viewport|PumpEnd::Severed|PumpEnd::BrokerGone|RECONNECT_WINDOW|read_event_until|NetStreamEof|heartbeat|idle","path":"crates/spt/src/rc.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_qHHAdbaHrV0Qr3PDVNmIgw0N|fc_0698befac979932d016a58ab1d1bec819ab22d46ce2c56420c","name":"read","arguments":{"i":"Reading current wedge coderead","path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","selector":"1-230"}},{"type":"toolCall","id":"call_cgzugNL7kC1dWrsotrgysZPQ|fc_0698befac979932d016a58ab1d1bf0819ab678363ed2881162","name":"read","arguments":{"i":"Reading current lifecycle incident summary","path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","selector":"35-105"}},{"type":"toolCall","id":"call_HkJWWR9iRxgjsLxXvLRiByOi|fc_0698befac979932d016a58ab1d1bf4819ab058b7cec94f61d2","name":"read","arguments":{"i":"Reading broker brain split rules","path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","selector":"1-165,260-355"}},{"type":"toolCall","id":"call_ZQfl2LKDfOKTMsOW0hgGvN6U|fc_0698befac979932d016a58ab1d1bfc819a8cb89e5273d57e54","name":"read","arguments":{"i":"Reading debug rollout broker rules","path":"docs/DEBUG-ROLLOUT.md","selector":"1-95"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":41845,"output":714,"cacheRead":76288,"cacheWrite":0,"totalTokens":118847,"reasoningTokens":244,"cost":{"input":0.20922500000000002,"output":0.02142,"cacheRead":0.038144,"cacheWrite":0,"total":0.268789}},"stopReason":"toolUse","timestamp":1784195850577,"responseId":"resp_0698befac979932d016a58ab0af314819aa7f063e446bafabf","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ab0d395c819a9f6f2396f0a5263b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsd9j5q98NaWXPzHvtWURf5N52Yrx0j9145DLrDX0kRALRDAeDzUtWMXd2JItRTX-ZupJl7Cq1qcMAcPCEC1REWdYEaH0prX7MXv2-o9ePcMKCjPzwhpGCXe_tRtNUfGlWWQjZCyhu9jvD1dMBD-cjRbSWlnunbtoIB1EgbsEJbovDoG93ct6tH5qdv235Rh6YehIOutVhT9hkmpXb_sT5jxrNmmgKb92kw-5ywgkDczkE_BHXtBzNUYsWZhzzhI-VYXnTbMAsM6juWSPVzYecRRuLnayw72Kaz78wLp__Jc78l4ieuCLG2JR0qvS1g6z54jfNOMsdc0qfvk9NB-g5Mv075dTA61vgz_UjSq-6pHNcMFBc_bWawPu7DS2VjcxtFrCjhAIMU65kz7EVm4IvmeYUqp2er28bnRuYVGbSuSHtnc1xzIkw8qAru6wW2JpEkQiA4u--NU-nllc8utPY9GTpsuFDOOwfhaeXuEfhgHjPp7Yfk38VnFkJ_vs5DJ5nSILURG0GR1AvcLW9TNekCKOIKaehaPJV3538sS2yap9eDIHip7lbz4O9bJaTqGiQnys7NAxoZwkdz2iQ31oIv0AMYifXBZGPLLpdL2elist5KaeojwLVqPYIKQlEPKkgmjofvTJU75XALE1oKZ724T9HTFq_XZ_wfFfVX99FdT97q_zlXYL9Qrrm9QnMy6YDUFX-sNIxJf4UQ1mxqdnUr874uu4VzlC3d8xB952EdpMW1hy3BlWZok8JmF1twIT2beBQHRzpFgJZzTCQZDstuEjO3ZYWULQOE6zJ50NSWDgvnmzaC-1kEZ-dXvHkRx3uebKDkvzevVTYe7URHD7yHRh0bUnqZhpDw4EFILtSFw10VYkXGn7yU3BvY4RKOXAyjtNJP2uYBHKt1wvtOLnZHsfH5SKOqcmT3ysB3fawkDtlw-k1BIZ1sLx3UhDEZgjTH9rnErmC0yxo4bGi66Y8Z-IZxCBD4HBeP795TiiwoAGnaow-85nMBv7CZsM4eseoP4ukVXBljAXrCsPMJmp8D8gr9-SywJCs9SD0cmGqP6St45hMA4SnF2_EO2IF3y1gUVo4wbpeb9Qv56VrghVQcTtbVB9sEAFyjpi8eu5qnxeZRc5ixB3LsBiX05xyQ1zMMNT1Jfet_uW6zfs4t1bOvujVSK3hJZxEwmfsXVWBWPlpF4HU-Q0mgdKm5hKjyC14nuXZwnORERZ0MEZJXI73gVhuPOcx9j15dl00NGRe-ixU8vUYSRhN-Hvplf_Fuso4s5otFsVSO6cYd5Bs-Z80CUs52-UkdJXuDofTHYuWy99xfm4yesn40UtdIcU-NvLmrVbQeedaz3SAlkOhVYvCbDGjMOxWsz8NtlDlo73G04MQj90NPHQDpzvXEuPrzU1nIKMAjMfNak8T4XigeZw-pAbgVodL0-OX95txvd7uOSyYNshKsuXgagMNw8E7r-WRfXs1Fig2Ez7dlmxC3I29vxmvLb0PdeooH9vgtOoOK6O6sniVFFvMr3IncTFGMNHA06zEsi7UMVPPeP_U1MZOKB2RLwNalUSQ3E6Dnqwgkk0bLo3S8KbnvSCkYOBSvlH7LJaO0tIqv6bwAH0f0O1gKaSuMBgLr3_2WKNHYjRlluwb-hPLM4AZ_wCivq88AOEKjYkf-UnMpVNDWg_vEBUQNu7G3D8UwuOI-04QSyPCNw-LAvj1cDhsLQBe34iyHpd0HwrbhUHWwoNo4wGR85N6-E15NwxiOl3QzaO7UXODrTO2dzRaHkGn05JJNUh6e6asoOmyBZZEbA-o_Vn4SOK46doFSZUO1s0n7cJeadBEsQuXAk6m_RXRraoRtg-JA6ag4vwd3y7_Le8gCfT0xQptNnRttAljJ3Jo04sZojniPrCQMVcFYDHw7edBzdoD-Isgqp0Ej-2D_V8rowWM_T2OxlfC8MUxAWatnaGvN4abYVyOPAmJjQAogbNdfFH0TqirUUtklYDNeA-zcrZewqKqbFM7u5TTA45GgjYm1J6dbrIq7s_CYyjBAcQ-eX-ULOb7pxE_hejgVc0sbBLv_M4mesm4xVV4xHONXXMNOFdHBb6CtyOvFvztIFfJHEoqkNNes8O6AOFtqIiGWesLkE7QmXBSUtcwHoQ35QaZdWqXRtshnj7O8mg5Ake98kpF5db2ZlM2bmS07omPKUChHQ7XDsoKPNpGK95ZKn-Qa1s4NWQB1e1EycVTVUICCg_MPywHu4IvFcwVGq_cs963YqGu5SOikm_H6zCE-dQLyodrfzb-63Nf3MYI-RIHCUWKcRHynWKOLz48HSum_ojtpTgCkEaHSN6shvyEuGmIWgJL-TdMFJeRGqExWptS1Jtrnrs2jv07eWjvbGLhVzixNfEq9FG-xy5Iy9jYRQTwG_-uZvx5qBSQWq0afSYgPYHtBk9fkwKaWIQFh6kvBjUZk-dzVkG5aftytDZrl787xf4dUZK91PaHX2LBQNSjFjnHjRg1OTeSRlT2EGNbD5p81_kSva_f1L8z3QD6q2WJepLlYlFQvIOPcxo1FiEiVW4iV13meezEMJGkaaMVfUPcn2yrtDY4mFjMvAimI3vV2xtZM0ls=","summary":[{"type":"summary_text","text":"**Diagnosing serve_attach thread discrepancies**"},{"type":"summary_text","text":"**Tracing dispatcher thread behavior**"}]},{"id":"fc_0698befac979932d016a58ab1d1bc8819a970eee858c219ed5","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing attach dispatcher ownership\",\"pattern\":\"serve_attach\\\\(|spawn_inbound|run_inbound|dispatch_inbound|KIND_NET_STREAM|NetStreamOpened|AttachRecord::Request|spawn_net_consumers\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_OGvVXD0WEZdBccfk90udttzr","name":"grep"},{"id":"fc_0698befac979932d016a58ab1d1bdc819ab380355b3db47c05","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding stream lifecycle on conn drop\",\"pattern\":\"remove.*stream|finish_stream|NetStreamEof|net_stream.*finish|stream.*closed|close.*stream|drop.*stream|unsubscribe.*stream\",\"path\":\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_XvN7ZG6TEum29Cn1U1hmHL0P","name":"grep"},{"id":"fc_0698befac979932d016a58ab1d1be4819abdc4200c61358bbd","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding rc liveness and reconnect trigger\",\"pattern\":\"attach_viewport|PumpEnd::Severed|PumpEnd::BrokerGone|RECONNECT_WINDOW|read_event_until|NetStreamEof|heartbeat|idle\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_4j4OX07YHY0bh5sxkQSoNUbp","name":"grep"},{"id":"fc_0698befac979932d016a58ab1d1bec819ab22d46ce2c56420c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current wedge coderead\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\",\"selector\":\"1-230\"}","call_id":"call_qHHAdbaHrV0Qr3PDVNmIgw0N","name":"read"},{"id":"fc_0698befac979932d016a58ab1d1bf0819ab678363ed2881162","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current lifecycle incident summary\",\"path\":\"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\",\"selector\":\"35-105\"}","call_id":"call_cgzugNL7kC1dWrsotrgysZPQ","name":"read"},{"id":"fc_0698befac979932d016a58ab1d1bf4819ab058b7cec94f61d2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker brain split rules\",\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"selector\":\"1-165,260-355\"}","call_id":"call_HkJWWR9iRxgjsLxXvLRiByOi","name":"read"},{"id":"fc_0698befac979932d016a58ab1d1bfc819a8cb89e5273d57e54","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading debug rollout broker rules\",\"path\":\"docs/DEBUG-ROLLOUT.md\",\"selector\":\"1-95\"}","call_id":"call_ZQfl2LKDfOKTMsOW0hgGvN6U","name":"read"}]},"duration":19069.574400000274,"ttft":2624.250900000334,"contextSnapshot":{"promptTokens":118133,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OGvVXD0WEZdBccfk90udttzr|fc_0698befac979932d016a58ab1d1bc8819a970eee858c219ed5","toolName":"grep","startedAt":"2026-07-16T09:57:49.648Z","args":{"path":"crates/spt-daemon/src"},"intent":"Tracing attach dispatcher ownership"},"id":"589a040d","parentId":"3c1ceea2","timestamp":"2026-07-16T09:57:49.648Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XvN7ZG6TEum29Cn1U1hmHL0P|fc_0698befac979932d016a58ab1d1bdc819ab380355b3db47c05","toolName":"grep","startedAt":"2026-07-16T09:57:49.648Z","args":{"path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs"},"intent":"Finding stream lifecycle on conn drop"},"id":"34bb7cd8","parentId":"589a040d","timestamp":"2026-07-16T09:57:49.648Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4j4OX07YHY0bh5sxkQSoNUbp|fc_0698befac979932d016a58ab1d1be4819abdc4200c61358bbd","toolName":"grep","startedAt":"2026-07-16T09:57:49.649Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Finding rc liveness and reconnect trigger"},"id":"3e99bf66","parentId":"34bb7cd8","timestamp":"2026-07-16T09:57:49.649Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qHHAdbaHrV0Qr3PDVNmIgw0N|fc_0698befac979932d016a58ab1d1bec819ab22d46ce2c56420c","toolName":"read","startedAt":"2026-07-16T09:57:49.649Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md"},"intent":"Reading current wedge coderead"},"id":"372ebc1f","parentId":"3e99bf66","timestamp":"2026-07-16T09:57:49.649Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cgzugNL7kC1dWrsotrgysZPQ|fc_0698befac979932d016a58ab1d1bf0819ab678363ed2881162","toolName":"read","startedAt":"2026-07-16T09:57:49.649Z","args":{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md"},"intent":"Reading current lifecycle incident summary"},"id":"169e4511","parentId":"372ebc1f","timestamp":"2026-07-16T09:57:49.649Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HkJWWR9iRxgjsLxXvLRiByOi|fc_0698befac979932d016a58ab1d1bf4819ab058b7cec94f61d2","toolName":"read","startedAt":"2026-07-16T09:57:49.649Z","args":{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md"},"intent":"Reading broker brain split rules"},"id":"2698ae8a","parentId":"169e4511","timestamp":"2026-07-16T09:57:49.649Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZQfl2LKDfOKTMsOW0hgGvN6U|fc_0698befac979932d016a58ab1d1bfc819a8cb89e5273d57e54","toolName":"read","startedAt":"2026-07-16T09:57:49.649Z","args":{"path":"docs/DEBUG-ROLLOUT.md"},"intent":"Reading debug rollout broker rules"},"id":"1d6786c8","parentId":"2698ae8a","timestamp":"2026-07-16T09:57:49.649Z"}
{"type":"message","id":"ac50492e","parentId":"1d6786c8","timestamp":"2026-07-16T09:57:49.652Z","message":{"role":"toolResult","toolCallId":"call_4j4OX07YHY0bh5sxkQSoNUbp|fc_0698befac979932d016a58ab1d1be4819abdc4200c61358bbd","toolName":"grep","content":[{"type":"text","text":" 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 1213|    let mut stdout = std::io::stdout();\n*1214|    let outcome = attach_viewport(\n 1215|        endpoint_id,\n 1216|        intent,\n 1217|        view,\n...\n 1263|                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n*1264|                    RECONNECT_WINDOW.as_secs()\n 1265|                );\n 1266|            } else {\n 1267|                let _ = writeln!(\n...\n 1271|                     {endpoint_id}` again once the node/daemon is back]\",\n*1272|                    RECONNECT_WINDOW.as_secs()\n 1273|                );\n 1274|            }\n 1275|            Ok(())\n...\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 1282|        }\n 1283|        Ok(PumpEnd::Stalled) => {\n...\n 1518|#[allow(clippy::too_many_arguments)]\n*1519|fn attach_viewport(\n 1520|    endpoint_id: &str,\n 1521|    intent: AttachIntent,\n 1522|    view: bool,\n...\n 1562|            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n*1563|                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n 1564|                _ => \"the connection was severed\",\n 1565|            },\n 1566|            _ => return end,\n...\n 1635|/// node's broker restart both land well inside it.\n*1636|const RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n 1637|/// The pause between reconnect attempts (each attempt already carries its own\n 1638|/// bounded setup deadlines — this just keeps the loop from hammering).\n 1639|const RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n...\n 1643|fn reconnect_expired(elapsed: Duration) -> bool {\n*1644|    elapsed >= RECONNECT_WINDOW\n 1645|}\n 1646|\n 1647|/// Seconds still left in the reconnect window, for the banner countdown\n*1648|/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n 1649|/// at the start and never flashes `0s` while the loop is still trying (0 only once\n 1650|/// the window is fully spent, where the caller has already broken to give-up).\n 1651|// [impl->REQ-RC-RECONNECT-TRUTH]\n 1652|fn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n*1653|    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n 1654|    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n 1655|    (left.as_millis() as u64).div_ceil(1000)\n 1656|}\n...\n 1672|    match end {\n*1673|        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n 1674|        PumpEnd::Exited(_)\n 1675|        | PumpEnd::Detached\n 1676|        | PumpEnd::Displaced(_)\n...\n 1690|    if rendered_any {\n*1691|        PumpEnd::Severed\n 1692|    } else {\n 1693|        PumpEnd::NoLiveSession\n 1694|    }\n...\n 1778|\n*1779|/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n 1780|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 1781|enum ReadDisposition {\n 1782|    /// Transient poll-slice timeout — loop and poll again.\n...\n 1790|\n*1791|/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n 1792|/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n 1793|/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n 1794|/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n...\n 1914|        // ── session → terminal ─────────────────────────────────────────────\n*1915|        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n 1916|            Ok(ev) => ev,\n 1917|            Err(e) => match classify_read_err(e.kind()) {\n 1918|                // A poll-slice timeout: no event this slice. Before looping, the\n...\n 1928|                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n*1929|                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n 1930|                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n 1931|            },\n 1932|        };\n...\n 1991|            }\n*1992|            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 1993|                return Ok(serve_eof_end(rendered_any));\n 1994|            }\n 1995|            BrokerEvent::Error { message } => return Err(message),\n...\n 2143|    #[allow(clippy::zombie_processes)]\n*2144|    fn attach_viewport_reconnects_across_a_broker_bounce() {\n 2145|        let _home = crate::testutil::isolated_home();\n 2146|        let _seed = SeedGuard::start();\n 2147|        let broker_name = spt_daemon::endpoint::broker_socket_name();\n...\n 2266|            let mut remote = est.remote_node.clone();\n*2267|            attach_viewport(\n 2268|                \"reheal\",\n 2269|                AttachIntent::Viewer,\n 2270|                true,\n...\n 2390|    fn reconnect_class_only_severed_transport_redrives() {\n*2391|        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n*2392|        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n 2393|        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n 2394|        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n 2395|        assert_eq!(\n...\n 2415|    fn serve_eof_severed_vs_refused() {\n*2416|        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n 2417|        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n 2418|    }\n 2419|\n...\n 2424|        assert!(!reconnect_expired(Duration::from_secs(0)));\n*2425|        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n*2426|        assert!(reconnect_expired(RECONNECT_WINDOW));\n*2427|        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\n 2428|    }\n 2429|\n 2430|    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\n...\n 2464|    // [unit->REQ-RC-RECONNECT-TRUTH] the countdown derivation: CEIL of the window\n*2465|    // remainder — a full RECONNECT_WINDOW at t=0, decrementing, and 0 only once the\n 2466|    // window is fully spent (the loop has already broken to give-up by then, so the\n 2467|    // operator never sees a live \"0s\"). Unit-tested without a 30s wait.\n 2468|    #[test]\n...\n 2471|            reconnect_remaining_secs(Duration::ZERO),\n*2472|            RECONNECT_WINDOW.as_secs()\n 2473|        );\n 2474|        // 0.5s in ⇒ 29.5s left ⇒ ceil = 30 (still shows the full window briefly).\n 2475|        assert_eq!(\n 2476|            reconnect_remaining_secs(Duration::from_millis(500)),\n*2477|            RECONNECT_WINDOW.as_secs()\n 2478|        );\n 2479|        // 1s in ⇒ exactly 29s left ⇒ 29.\n 2480|        assert_eq!(\n 2481|            reconnect_remaining_secs(Duration::from_secs(1)),\n*2482|            RECONNECT_WINDOW.as_secs() - 1\n 2483|        );\n 2484|        // 29.001s in ⇒ ~0.999s left ⇒ ceil = 1 (never flashes 0 while trying).\n 2485|        assert_eq!(\n*2486|            reconnect_remaining_secs(RECONNECT_WINDOW - Duration::from_millis(1)),\n 2487|            1\n 2488|        );\n 2489|        // At/past the window ⇒ 0 (give-up territory).\n*2490|        assert_eq!(reconnect_remaining_secs(RECONNECT_WINDOW), 0);\n 2491|        assert_eq!(\n*2492|            reconnect_remaining_secs(RECONNECT_WINDOW + Duration::from_secs(5)),\n 2493|            0\n 2494|        );\n 2495|    }\n...\n 2509|                     `spt daemon start`, then `spt rc doyle`]\",\n*2510|                    RECONNECT_WINDOW.as_secs()\n 2511|                )\n 2512|            } else {\n 2513|                format!(\n...\n 2516|                     once the node/daemon is back]\",\n*2517|                    RECONNECT_WINDOW.as_secs()\n 2518|                )\n 2519|            }\n 2520|        };"}],"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":35,"fileCount":1,"files":["crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":35}],"truncated":false,"displayContent":"   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 1213│    let mut stdout = std::io::stdout();\n*1214│    let outcome = attach_viewport(\n 1215│        endpoint_id,\n 1216│        intent,\n 1217│        view,\n     │...\n 1263│                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n*1264│                    RECONNECT_WINDOW.as_secs()\n 1265│                );\n 1266│            } else {\n 1267│                let _ = writeln!(\n     │...\n 1271│                     {endpoint_id}` again once the node/daemon is back]\",\n*1272│                    RECONNECT_WINDOW.as_secs()\n 1273│                );\n 1274│            }\n 1275│            Ok(())\n     │...\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 1282│        }\n 1283│        Ok(PumpEnd::Stalled) => {\n     │...\n 1518│#[allow(clippy::too_many_arguments)]\n*1519│fn attach_viewport(\n 1520│    endpoint_id: &str,\n 1521│    intent: AttachIntent,\n 1522│    view: bool,\n     │...\n 1562│            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n*1563│                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n 1564│                _ => \"the connection was severed\",\n 1565│            },\n 1566│            _ => return end,\n     │...\n 1635│/// node's broker restart both land well inside it.\n*1636│const RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n 1637│/// The pause between reconnect attempts (each attempt already carries its own\n 1638│/// bounded setup deadlines — this just keeps the loop from hammering).\n 1639│const RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n     │...\n 1643│fn reconnect_expired(elapsed: Duration) -> bool {\n*1644│    elapsed >= RECONNECT_WINDOW\n 1645│}\n 1646│\n 1647│/// Seconds still left in the reconnect window, for the banner countdown\n*1648│/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n 1649│/// at the start and never flashes `0s` while the loop is still trying (0 only once\n 1650│/// the window is fully spent, where the caller has already broken to give-up).\n 1651│// [impl->REQ-RC-RECONNECT-TRUTH]\n 1652│fn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n*1653│    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n 1654│    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n 1655│    (left.as_millis() as u64).div_ceil(1000)\n 1656│}\n     │...\n 1672│    match end {\n*1673│        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n 1674│        PumpEnd::Exited(_)\n 1675│        | PumpEnd::Detached\n 1676│        | PumpEnd::Displaced(_)\n     │...\n 1690│    if rendered_any {\n*1691│        PumpEnd::Severed\n 1692│    } else {\n 1693│        PumpEnd::NoLiveSession\n 1694│    }\n     │...\n 1778│\n*1779│/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n 1780│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 1781│enum ReadDisposition {\n 1782│    /// Transient poll-slice timeout — loop and poll again.\n     │...\n 1790│\n*1791│/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n 1792│/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n 1793│/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n 1794│/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n     │...\n 1914│        // ── session → terminal ─────────────────────────────────────────────\n*1915│        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n 1916│            Ok(ev) => ev,\n 1917│            Err(e) => match classify_read_err(e.kind()) {\n 1918│                // A poll-slice timeout: no event this slice. Before looping, the\n     │...\n 1928│                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n*1929│                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n 1930│                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n 1931│            },\n 1932│        };\n     │...\n 1991│            }\n*1992│            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 1993│                return Ok(serve_eof_end(rendered_any));\n 1994│            }\n 1995│            BrokerEvent::Error { message } => return Err(message),\n     │...\n 2143│    #[allow(clippy::zombie_processes)]\n*2144│    fn attach_viewport_reconnects_across_a_broker_bounce() {\n 2145│        let _home = crate::testutil::isolated_home();\n 2146│        let _seed = SeedGuard::start();\n 2147│        let broker_name = spt_daemon::endpoint::broker_socket_name();\n     │...\n 2266│            let mut remote = est.remote_node.clone();\n*2267│            attach_viewport(\n 2268│                \"reheal\",\n 2269│                AttachIntent::Viewer,\n 2270│                true,\n     │...\n 2390│    fn reconnect_class_only_severed_transport_redrives() {\n*2391│        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n*2392│        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n 2393│        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n 2394│        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n 2395│        assert_eq!(\n     │...\n 2415│    fn serve_eof_severed_vs_refused() {\n*2416│        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n 2417│        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n 2418│    }\n 2419│\n     │...\n 2424│        assert!(!reconnect_expired(Duration::from_secs(0)));\n*2425│        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n*2426│        assert!(reconnect_expired(RECONNECT_WINDOW));\n*2427│        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\n 2428│    }\n 2429│\n 2430│    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\n     │...\n 2464│    // [unit->REQ-RC-RECONNECT-TRUTH] the countdown derivation: CEIL of the window\n*2465│    // remainder — a full RECONNECT_WINDOW at t=0, decrementing, and 0 only once the\n 2466│    // window is fully spent (the loop has already broken to give-up by then, so the\n 2467│    // operator never sees a live \"0s\"). Unit-tested without a 30s wait.\n 2468│    #[test]\n     │...\n 2471│            reconnect_remaining_secs(Duration::ZERO),\n*2472│            RECONNECT_WINDOW.as_secs()\n 2473│        );\n 2474│        // 0.5s in ⇒ 29.5s left ⇒ ceil = 30 (still shows the full window briefly).\n 2475│        assert_eq!(\n 2476│            reconnect_remaining_secs(Duration::from_millis(500)),\n*2477│            RECONNECT_WINDOW.as_secs()\n 2478│        );\n 2479│        // 1s in ⇒ exactly 29s left ⇒ 29.\n 2480│        assert_eq!(\n 2481│            reconnect_remaining_secs(Duration::from_secs(1)),\n*2482│            RECONNECT_WINDOW.as_secs() - 1\n 2483│        );\n 2484│        // 29.001s in ⇒ ~0.999s left ⇒ ceil = 1 (never flashes 0 while trying).\n 2485│        assert_eq!(\n*2486│            reconnect_remaining_secs(RECONNECT_WINDOW - Duration::from_millis(1)),\n 2487│            1\n 2488│        );\n 2489│        // At/past the window ⇒ 0 (give-up territory).\n*2490│        assert_eq!(reconnect_remaining_secs(RECONNECT_WINDOW), 0);\n 2491│        assert_eq!(\n*2492│            reconnect_remaining_secs(RECONNECT_WINDOW + Duration::from_secs(5)),\n 2493│            0\n 2494│        );\n 2495│    }\n     │...\n 2509│                     `spt daemon start`, then `spt rc doyle`]\",\n*2510│                    RECONNECT_WINDOW.as_secs()\n 2511│                )\n 2512│            } else {\n 2513│                format!(\n     │...\n 2516│                     once the node/daemon is back]\",\n*2517│                    RECONNECT_WINDOW.as_secs()\n 2518│                )\n 2519│            }\n 2520│        };"},"isError":false,"timestamp":1784195869652}}
{"type":"message","id":"cad950ef","parentId":"ac50492e","timestamp":"2026-07-16T09:57:49.659Z","message":{"role":"toolResult","toolCallId":"call_OGvVXD0WEZdBccfk90udttzr|fc_0698befac979932d016a58ab1d1bc8819a970eee858c219ed5","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## attach.rs\n 4|//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n*5|//! [`AttachRecord::Request`]; this module is the target-side pump between that\n 6|//! stream and the local broker-hosted session:\n 7|//!\n 8|//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n...\n 30|//! §Remote-control vs local operation). The access gate runs before anything\n*31|//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\n 32|//! [`access_check`] sees the **handshake-proven** origin from the broker's\n 33|//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n 34|//! your own running instance is ungated by default (ADR-0009 default-open);\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 332|                    match rec {\n*333|                        AttachRecord::Request {\n 334|                            session_id,\n 335|                            from_seq,\n 336|                            intent: req_intent,\n...\n 544|/// Operator-side: open an attach stream on a dialed connection and send the\n*545|/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n 546|/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n 547|/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n 548|/// durable id for the exactly-once stream open (safe to block for its ack —\n...\n 581|    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n*582|    let line = ndjson::encode_line(&AttachRecord::Request {\n 583|        session_id,\n 584|        from_seq,\n 585|        intent,\n## brain.rs\n 41|    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n*42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n 43|    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n 44|    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n 45|    ViewerEvictedEvent,\n...\n 50|    KIND_NET_SENT,\n*51|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*52|    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n*53|    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n 54|    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n 55|    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n 56|};\n...\n 146|    /// A stream open durably landed — fresh or deduped (D4b).\n*147|    NetStreamOpened(NetStreamOpened),\n 148|    /// A journaled stream send durably landed — fresh or deduped (D4b).\n 149|    NetSent(NetSent),\n 150|    /// The broker's held-streams listing (D4b).\n...\n 776|                KIND_NET_DIAL_SUBMITTED => return Ok(BrokerEvent::NetDialSubmitted),\n*777|                KIND_NET_STREAM_OPENED => {\n*778|                    let ev: NetStreamOpened = serde_json::from_value(env.payload)\n 779|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n*780|                    return Ok(BrokerEvent::NetStreamOpened(ev));\n 781|                }\n 782|                KIND_NET_SENT => {\n 783|                    let ev: NetSent = serde_json::from_value(env.payload)\n...\n 786|                }\n*787|                KIND_NET_STREAMS_REPLY => {\n 788|                    let ev: NetStreamsReply = serde_json::from_value(env.payload)\n 789|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 790|                    return Ok(BrokerEvent::NetStreams(ev));\n...\n 806|                }\n*807|                KIND_NET_STREAM_DATA => {\n 808|                    let ev: NetStreamData = serde_json::from_value(env.payload)\n 809|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 810|                    // Same exactly-once discipline as PTY output, per stream:\n...\n 834|                }\n*835|                KIND_NET_STREAM_EOF => {\n 836|                    let ev: NetStreamEof = serde_json::from_value(env.payload)\n 837|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 838|                    return Ok(BrokerEvent::NetStreamEof {\n...\n 1374|        op: Option<MintedOp>,\n*1375|    ) -> io::Result<NetStreamOpened> {\n 1376|        self.send(\n*1377|            KIND_NET_STREAM_OPEN,\n 1378|            serde_json::to_value(NetStreamOpenReq {\n 1379|                conn_id,\n 1380|                op_id: op.map(|o| o.seq),\n...\n 1387|            match self.read_event_until(deadline)? {\n*1388|                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\n 1389|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n 1390|                _ => continue,\n 1391|            }\n...\n 1407|        self.send(\n*1408|            KIND_NET_STREAM_SEND,\n 1409|            serde_json::to_value(NetStreamSendReq {\n 1410|                stream_id,\n 1411|                data_b64: encode_bytes(bytes),\n...\n 1434|    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n*1435|        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n 1436|        loop {\n 1437|            match self.read_event()? {\n 1438|                BrokerEvent::NetStreams(ev) => return Ok(ev),\n...\n 1533|        self.send(\n*1534|            KIND_NET_STREAM_SUBSCRIBE,\n 1535|            serde_json::to_value(NetStreamSubscribeReq {\n 1536|                stream_id,\n 1537|                from_seq,\n## brainproc.rs\n 272|            let hex = status.node_id_hex.clone().unwrap_or_default();\n*273|            spawn_net_consumers(&hex);\n 274|            consumers_started = true;\n 275|            eprintln!(\n 276|                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n...\n 320|// [impl->REQ-DAEMON-9]\n*321|fn spawn_net_consumers(node_hex: &str) {\n 322|    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323|    let stop = Arc::new(AtomicBool::new(false));\n 324|    let _ = crate::dispatch::spawn_dispatcher(\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 65|    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n*66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n...\n 3085|                }\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...\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 4202|            Envelope::new(\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 4294|        let frame = Envelope::new(\n*4295|            KIND_NET_STREAMS_REPLY,\n 4296|            serde_json::to_value(NetStreamsReply {\n 4297|                streams: host.stream_infos(),\n 4298|            })\n## dispatch.rs\n 101|    Registry,\n*102|    /// A remote-drive attach (`AttachRecord::Request`).\n 103|    Attach,\n 104|    /// A file transfer (`XferRecord::Fetch`/`Push`).\n 105|    Xfer,\n...\n 377|            match endpoint {\n*378|                Some(endpoint) => match serve_attach(\n 379|                    &mut brain,\n 380|                    broker_name,\n 381|                    stream_id,\n## msg.rs\n 125|/// `op_id` the open is exactly-once (journaled like a dial).\n*126|pub const KIND_NET_STREAM_OPEN: &str = \"net-stream-open\";\n 127|/// Broker→brain: a stream open durably landed (fresh or deduped).\n*128|pub const KIND_NET_STREAM_OPENED: &str = \"net-stream-opened\";\n 129|/// Brain→broker: write bytes to a held stream's send side (D4b). With an\n 130|/// `op_id` the write is exactly-once (journaled; acked with [`KIND_NET_SENT`]).\n*131|pub const KIND_NET_STREAM_SEND: &str = \"net-stream-send\";\n 132|/// Broker→brain: a journaled stream send durably landed (fresh or deduped).\n 133|pub const KIND_NET_SENT: &str = \"net-sent\";\n 134|/// Brain→broker: list the streams the broker currently holds (D4b). The query\n...\n 136|/// down — no announce buffer to miss.\n*137|pub const KIND_NET_STREAMS: &str = \"net-streams\";\n 138|/// Broker→brain: the held-streams reply.\n*139|pub const KIND_NET_STREAMS_REPLY: &str = \"net-streams-reply\";\n 140|/// Brain→broker: (re)subscribe to a stream's buffered reads from a cursor —\n 141|/// the PTY `subscribe` discipline applied to a QUIC stream (gapless resume).\n*142|pub const KIND_NET_STREAM_SUBSCRIBE: &str = \"net-stream-subscribe\";\n 143|/// Broker→brain: a sequenced chunk of a stream's reads (live or replayed).\n*144|pub const KIND_NET_STREAM_DATA: &str = \"net-stream-data\";\n 145|/// Broker→brain: a stream's read side finished cleanly; `next_seq` tells the\n 146|/// brain the final cursor so it can verify it missed nothing.\n*147|pub const KIND_NET_STREAM_EOF: &str = \"net-stream-eof\";\n 148|/// Brain→broker: (re)subscribe to the broker's conn-liveness event log from a\n 149|/// cursor (D4c) — the PresenceChannel broker seam. Same gapless-resume\n 150|/// discipline as a stream subscribe.\n...\n 705|#[derive(Debug, Clone, Serialize, Deserialize)]\n*706|pub struct NetStreamOpened {\n 707|    /// The broker-minted id addressing this stream in later commands.\n 708|    pub stream_id: u64,\n 709|    /// The connection it rides.\n...\n 734|    /// Finish the send side after this write (may carry empty bytes): the\n*735|    /// peer's read pump sees a clean end and emits [`KIND_NET_STREAM_EOF`].\n 736|    #[serde(default)]\n 737|    pub finish: bool,\n 738|}\n...\n 1060|    Envelope::new(\n*1061|        KIND_NET_STREAM_DATA,\n 1062|        serde_json::to_value(NetStreamData {\n 1063|            stream_id,\n 1064|            seq,\n...\n 1073|    Envelope::new(\n*1074|        KIND_NET_STREAM_EOF,\n 1075|        serde_json::to_value(NetStreamEof {\n 1076|            stream_id,\n 1077|            next_seq,"}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":50,"fileCount":6,"files":["crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/msg.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/attach.rs","count":6},{"path":"crates/spt-daemon/src/brain.rs","count":17},{"path":"crates/spt-daemon/src/brainproc.rs","count":2},{"path":"crates/spt-daemon/src/broker.rs","count":11},{"path":"crates/spt-daemon/src/dispatch.rs","count":2},{"path":"crates/spt-daemon/src/msg.rs","count":12}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## attach.rs\n   4│//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n  *5│//! [`AttachRecord::Request`]; this module is the target-side pump between that\n   6│//! stream and the local broker-hosted session:\n   7│//!\n   8│//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n    │...\n  30│//! §Remote-control vs local operation). The access gate runs before anything\n *31│//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so\n  32│//! [`access_check`] sees the **handshake-proven** origin from the broker's\n  33│//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n  34│//! your own running instance is ungated by default (ADR-0009 default-open);\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 332│                    match rec {\n*333│                        AttachRecord::Request {\n 334│                            session_id,\n 335│                            from_seq,\n 336│                            intent: req_intent,\n    │...\n 544│/// Operator-side: open an attach stream on a dialed connection and send the\n*545│/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n 546│/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n 547│/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n 548│/// durable id for the exactly-once stream open (safe to block for its ack —\n    │...\n 581│    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n*582│    let line = ndjson::encode_line(&AttachRecord::Request {\n 583│        session_id,\n 584│        from_seq,\n 585│        intent,\n## brain.rs\n   41│    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n  *42│    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n   43│    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n   44│    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n   45│    ViewerEvictedEvent,\n     │...\n   50│    KIND_NET_SENT,\n  *51│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  *52│    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n  *53│    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n   54│    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n   55│    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n   56│};\n     │...\n  146│    /// A stream open durably landed — fresh or deduped (D4b).\n *147│    NetStreamOpened(NetStreamOpened),\n  148│    /// A journaled stream send durably landed — fresh or deduped (D4b).\n  149│    NetSent(NetSent),\n  150│    /// The broker's held-streams listing (D4b).\n     │...\n  776│                KIND_NET_DIAL_SUBMITTED => return Ok(BrokerEvent::NetDialSubmitted),\n *777│                KIND_NET_STREAM_OPENED => {\n *778│                    let ev: NetStreamOpened = serde_json::from_value(env.payload)\n  779│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n *780│                    return Ok(BrokerEvent::NetStreamOpened(ev));\n  781│                }\n  782│                KIND_NET_SENT => {\n  783│                    let ev: NetSent = serde_json::from_value(env.payload)\n     │...\n  786│                }\n *787│                KIND_NET_STREAMS_REPLY => {\n  788│                    let ev: NetStreamsReply = serde_json::from_value(env.payload)\n  789│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  790│                    return Ok(BrokerEvent::NetStreams(ev));\n     │...\n  806│                }\n *807│                KIND_NET_STREAM_DATA => {\n  808│                    let ev: NetStreamData = serde_json::from_value(env.payload)\n  809│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  810│                    // Same exactly-once discipline as PTY output, per stream:\n     │...\n  834│                }\n *835│                KIND_NET_STREAM_EOF => {\n  836│                    let ev: NetStreamEof = serde_json::from_value(env.payload)\n  837│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  838│                    return Ok(BrokerEvent::NetStreamEof {\n     │...\n 1374│        op: Option<MintedOp>,\n*1375│    ) -> io::Result<NetStreamOpened> {\n 1376│        self.send(\n*1377│            KIND_NET_STREAM_OPEN,\n 1378│            serde_json::to_value(NetStreamOpenReq {\n 1379│                conn_id,\n 1380│                op_id: op.map(|o| o.seq),\n     │...\n 1387│            match self.read_event_until(deadline)? {\n*1388│                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\n 1389│                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n 1390│                _ => continue,\n 1391│            }\n     │...\n 1407│        self.send(\n*1408│            KIND_NET_STREAM_SEND,\n 1409│            serde_json::to_value(NetStreamSendReq {\n 1410│                stream_id,\n 1411│                data_b64: encode_bytes(bytes),\n     │...\n 1434│    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n*1435│        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n 1436│        loop {\n 1437│            match self.read_event()? {\n 1438│                BrokerEvent::NetStreams(ev) => return Ok(ev),\n     │...\n 1533│        self.send(\n*1534│            KIND_NET_STREAM_SUBSCRIBE,\n 1535│            serde_json::to_value(NetStreamSubscribeReq {\n 1536│                stream_id,\n 1537│                from_seq,\n## brainproc.rs\n 272│            let hex = status.node_id_hex.clone().unwrap_or_default();\n*273│            spawn_net_consumers(&hex);\n 274│            consumers_started = true;\n 275│            eprintln!(\n 276│                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n    │...\n 320│// [impl->REQ-DAEMON-9]\n*321│fn spawn_net_consumers(node_hex: &str) {\n 322│    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323│    let stop = Arc::new(AtomicBool::new(false));\n 324│    let _ = crate::dispatch::spawn_dispatcher(\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   65│    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n  *66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  *67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n   69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n   70│};\n     │...\n 3085│                }\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     │...\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 4202│            Envelope::new(\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 4294│        let frame = Envelope::new(\n*4295│            KIND_NET_STREAMS_REPLY,\n 4296│            serde_json::to_value(NetStreamsReply {\n 4297│                streams: host.stream_infos(),\n 4298│            })\n## dispatch.rs\n 101│    Registry,\n*102│    /// A remote-drive attach (`AttachRecord::Request`).\n 103│    Attach,\n 104│    /// A file transfer (`XferRecord::Fetch`/`Push`).\n 105│    Xfer,\n    │...\n 377│            match endpoint {\n*378│                Some(endpoint) => match serve_attach(\n 379│                    &mut brain,\n 380│                    broker_name,\n 381│                    stream_id,\n## msg.rs\n  125│/// `op_id` the open is exactly-once (journaled like a dial).\n *126│pub const KIND_NET_STREAM_OPEN: &str = \"net-stream-open\";\n  127│/// Broker→brain: a stream open durably landed (fresh or deduped).\n *128│pub const KIND_NET_STREAM_OPENED: &str = \"net-stream-opened\";\n  129│/// Brain→broker: write bytes to a held stream's send side (D4b). With an\n  130│/// `op_id` the write is exactly-once (journaled; acked with [`KIND_NET_SENT`]).\n *131│pub const KIND_NET_STREAM_SEND: &str = \"net-stream-send\";\n  132│/// Broker→brain: a journaled stream send durably landed (fresh or deduped).\n  133│pub const KIND_NET_SENT: &str = \"net-sent\";\n  134│/// Brain→broker: list the streams the broker currently holds (D4b). The query\n     │...\n  136│/// down — no announce buffer to miss.\n *137│pub const KIND_NET_STREAMS: &str = \"net-streams\";\n  138│/// Broker→brain: the held-streams reply.\n *139│pub const KIND_NET_STREAMS_REPLY: &str = \"net-streams-reply\";\n  140│/// Brain→broker: (re)subscribe to a stream's buffered reads from a cursor —\n  141│/// the PTY `subscribe` discipline applied to a QUIC stream (gapless resume).\n *142│pub const KIND_NET_STREAM_SUBSCRIBE: &str = \"net-stream-subscribe\";\n  143│/// Broker→brain: a sequenced chunk of a stream's reads (live or replayed).\n *144│pub const KIND_NET_STREAM_DATA: &str = \"net-stream-data\";\n  145│/// Broker→brain: a stream's read side finished cleanly; `next_seq` tells the\n  146│/// brain the final cursor so it can verify it missed nothing.\n *147│pub const KIND_NET_STREAM_EOF: &str = \"net-stream-eof\";\n  148│/// Brain→broker: (re)subscribe to the broker's conn-liveness event log from a\n  149│/// cursor (D4c) — the PresenceChannel broker seam. Same gapless-resume\n  150│/// discipline as a stream subscribe.\n     │...\n  705│#[derive(Debug, Clone, Serialize, Deserialize)]\n *706│pub struct NetStreamOpened {\n  707│    /// The broker-minted id addressing this stream in later commands.\n  708│    pub stream_id: u64,\n  709│    /// The connection it rides.\n     │...\n  734│    /// Finish the send side after this write (may carry empty bytes): the\n *735│    /// peer's read pump sees a clean end and emits [`KIND_NET_STREAM_EOF`].\n  736│    #[serde(default)]\n  737│    pub finish: bool,\n  738│}\n     │...\n 1060│    Envelope::new(\n*1061│        KIND_NET_STREAM_DATA,\n 1062│        serde_json::to_value(NetStreamData {\n 1063│            stream_id,\n 1064│            seq,\n     │...\n 1073│    Envelope::new(\n*1074│        KIND_NET_STREAM_EOF,\n 1075│        serde_json::to_value(NetStreamEof {\n 1076│            stream_id,\n 1077│            next_seq,"},"isError":false,"timestamp":1784195869659}}
{"type":"message","id":"086fcf85","parentId":"cad950ef","timestamp":"2026-07-16T09:57:49.660Z","message":{"role":"toolResult","toolCallId":"call_HkJWWR9iRxgjsLxXvLRiByOi|fc_0698befac979932d016a58ab1d1bf4819ab058b7cec94f61d2","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[Range 260-355 is beyond end of file (38 lines total); skipped]\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"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","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":1784195869660}}
{"type":"message","id":"95b45f44","parentId":"086fcf85","timestamp":"2026-07-16T09:57:49.666Z","message":{"role":"toolResult","toolCallId":"call_cgzugNL7kC1dWrsotrgysZPQ|fc_0698befac979932d016a58ab1d1bf0819ab678363ed2881162","toolName":"read","content":[{"type":"text","text":"## W1 — psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)\n…\n**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)\n…\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\n…\nInt: two concurrent wake requests → exactly one launch tree.\n…\n\n\n[Showing lines 34-108 of 219. Use :109 to continue]"}],"details":{"truncation":{"content":"(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","truncated":true,"truncatedBy":"lines","totalLines":186,"totalBytes":5133,"outputLines":75,"outputBytes":5133,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"## W1 — psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)\n…\n**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)\n…\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\n…\nInt: two concurrent wake requests → exactly one launch tree.\n…\n","startLine":18,"lineNumbers":[18,null,32,null,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,null,115,null,125]},"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":5133,"outputLines":75,"outputBytes":5133,"shownRange":{"start":34,"end":108},"nextOffset":109}}},"isError":false,"timestamp":1784195869666}}
{"type":"message","id":"64665eba","parentId":"95b45f44","timestamp":"2026-07-16T09:57:49.667Z","message":{"role":"toolResult","toolCallId":"call_ZQfl2LKDfOKTMsOW0hgGvN6U|fc_0698befac979932d016a58ab1d1bfc819a8cb89e5273d57e54","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…\n\n…\n5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.\n\n[Showing lines 1-98 of 121. Use :99 to continue]"}],"details":{"truncation":{"content":"# 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","truncated":true,"truncatedBy":"lines","totalLines":121,"totalBytes":3856,"outputLines":98,"outputBytes":3856,"lastLinePartial":false,"firstLineExceedsLimit":false},"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.\n…\n\n…\n5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.","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,null,110,null,120]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\DEBUG-ROLLOUT.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":121,"totalBytes":3856,"outputLines":98,"outputBytes":3856,"shownRange":{"start":1,"end":98},"nextOffset":99}}},"isError":false,"timestamp":1784195869666}}
{"type":"message","id":"c0c5f7d4","parentId":"64665eba","timestamp":"2026-07-16T09:57:49.667Z","message":{"role":"toolResult","toolCallId":"call_qHHAdbaHrV0Qr3PDVNmIgw0N|fc_0698befac979932d016a58ab1d1bec819ab22d46ce2c56420c","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":1784195869667}}
{"type":"message","id":"089af579","parentId":"c0c5f7d4","timestamp":"2026-07-16T09:57:49.682Z","message":{"role":"toolResult","toolCallId":"call_XvN7ZG6TEum29Cn1U1hmHL0P|fc_0698befac979932d016a58ab1d1bdc819ab380355b3db47c05","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## nethost.rs\n 108|/// the PTY `OutputLog`: large enough that a brain-restart window never evicts\n*109|/// unreplayed chunks in practice; bounded so an unsubscribed stream can't\n 110|/// buffer without limit.\n 111|const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n 112|\n...\n 981|    /// unbounded await that wedged the single-threaded pump for 2.2h. On elapse\n*982|    /// the future is DROPPED — cancelling the in-flight connect / stream op, so\n 983|    /// nothing is half-registered — and a non-`TimedOut` `io::Error` is returned.\n 984|    /// At the broker boundary that error is stringified into a normal error reply\n 985|    /// frame, so the brain reconstructs an `ErrorKind::Other` (`Brain::net_dial` /\n## dispatch.rs\n 271|            }\n*272|            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 273|                // Finished without a complete line: classify what we have\n 274|                // (a line without a trailing newline before EOF still counts).\n 275|                return Ok(serde_json::from_slice(&buf).ok());\n...\n 529|            }\n*530|            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 531|                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n 532|            }\n 533|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n...\n 588|            }\n*589|            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 590|                return DispatchOutcome::Served(format!(\"notif feed: {applied} record(s)\"));\n 591|            }\n 592|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n...\n 673|            }\n*674|            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 675|                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n 676|            }\n 677|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n## attach.rs\n 494|            // The operator closed its send side: the viewport is over.\n*495|            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 496|                wire.net_stream_send(stream_id, &[], None, true)?;\n 497|                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n 498|                // info.json, carrying the current driven_by through), THEN release"}],"details":{"scopePath":"crates/spt-daemon/src/nethost.rs, crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/dispatch.rs, crates/spt-daemon/src/attach.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":7,"fileCount":3,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/attach.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":2},{"path":"crates/spt-daemon/src/dispatch.rs","count":4},{"path":"crates/spt-daemon/src/attach.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## nethost.rs\n 108│/// the PTY `OutputLog`: large enough that a brain-restart window never evicts\n*109│/// unreplayed chunks in practice; bounded so an unsubscribed stream can't\n 110│/// buffer without limit.\n 111│const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n 112│\n    │...\n 981│    /// unbounded await that wedged the single-threaded pump for 2.2h. On elapse\n*982│    /// the future is DROPPED — cancelling the in-flight connect / stream op, so\n 983│    /// nothing is half-registered — and a non-`TimedOut` `io::Error` is returned.\n 984│    /// At the broker boundary that error is stringified into a normal error reply\n 985│    /// frame, so the brain reconstructs an `ErrorKind::Other` (`Brain::net_dial` /\n## dispatch.rs\n 271│            }\n*272│            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 273│                // Finished without a complete line: classify what we have\n 274│                // (a line without a trailing newline before EOF still counts).\n 275│                return Ok(serde_json::from_slice(&buf).ok());\n    │...\n 529│            }\n*530│            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 531│                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n 532│            }\n 533│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n    │...\n 588│            }\n*589│            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 590│                return DispatchOutcome::Served(format!(\"notif feed: {applied} record(s)\"));\n 591│            }\n 592│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n    │...\n 673│            }\n*674│            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 675│                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n 676│            }\n 677│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n## attach.rs\n 494│            // The operator closed its send side: the viewport is over.\n*495│            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 496│                wire.net_stream_send(stream_id, &[], None, true)?;\n 497│                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n 498│                // info.json, carrying the current driven_by through), THEN release"},"isError":false,"timestamp":1784195869682}}
{"type":"message","id":"f51c39d9","parentId":"089af579","timestamp":"2026-07-16T09:58:03.336Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Defining dispatch and cleanup behavior**"},{"type":"toolCall","id":"call_J8B5LDyBmJwPkqtgsSxpxnpk|fc_0698befac979932d016a58ab2abd58819ab9cdbc1beac42921","name":"read","arguments":{"i":"Reading dispatcher attach replay behavior","path":"crates/spt-daemon/src/dispatch.rs","selector":"300-430,700-830"}},{"type":"toolCall","id":"call_YJYlmTLUoeI9qQ26cwla3tYy|fc_0698befac979932d016a58ab2abda4819a81d090f4cdd8a7f7","name":"read","arguments":{"i":"Reading dispatcher supervisor boot","path":"crates/spt-daemon/src/brainproc.rs","selector":"315-375"}},{"type":"toolCall","id":"call_ZfAl6CQuQ9fxAGT1spD9rVd8|fc_0698befac979932d016a58ab2abdb0819abc8fc1582f331562","name":"read","arguments":{"i":"Reading stream subscribe cleanup","path":"crates/spt-daemon/src/broker.rs","selector":"3010-3210,4180-4330"}},{"type":"toolCall","id":"call_r2nkOwFpwyjA71zF5pGwxoX6|fc_0698befac979932d016a58ab2abdb8819a815e920dc370e847","name":"read","arguments":{"i":"Reading stream ownership internals","path":"crates/spt-daemon/src/nethost.rs","selector":"80-180,640-820,1210-1435"}},{"type":"toolCall","id":"call_rM3hgUyrT0fQ0lczENr7jSPU|fc_0698befac979932d016a58ab2abdc0819aad8603c19115b2c2","name":"read","arguments":{"i":"Reading rc idle stall behavior","path":"crates/spt/src/rc.rs","selector":"1510-1710,1770-2010"}},{"type":"toolCall","id":"call_lwSmY3D0I7Utx4lV2nMbeRZs|fc_0698befac979932d016a58ab2abe14819a8504d42e4ad92d26","name":"grep","arguments":{"i":"Finding dispatcher restart tests","pattern":"brain restart|restart|replay|resume|held stream|serve_attach","path":"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/src/dispatch.rs;crates/spt/tests/brain_survive.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_vF1WTNJii7Y9DM7unwqTfy9n|fc_0698befac979932d016a58ab2abe48819a8ac4ab86832b902f","name":"grep","arguments":{"i":"Finding refresh live rc coverage","pattern":"daemon refresh|refresh.*rc|rc.*refresh|attach.*refresh|viewport.*brain|controller.*refresh","path":"crates/spt/tests;crates/spt-daemon/tests;crates/spt/src/rc.rs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":16316,"output":427,"cacheRead":115200,"cacheWrite":0,"totalTokens":131943,"reasoningTokens":14,"cost":{"input":0.08158,"output":0.01281,"cacheRead":0.0576,"cacheWrite":0,"total":0.15199000000000001}},"stopReason":"toolUse","timestamp":1784195869685,"responseId":"resp_0698befac979932d016a58ab1e45cc819aaacce2f3984baece","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ab22efd4819a8c19b76742dcdc67","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsqxNwxV0BlIlToZM1u4bfSeZfw4z-SQmH-2-w0-dGBDvAywU9rgnlSpqoETQYXQSr88JbXJIR43M4MI23hjhrHZdYvJeNtdHPCcUlgXxOzKYeNE5_aiAVgZQMspO-DnemN0QBkkFO77JioKw6HMclEbXK67O9cJwyZRkgNqhFUTQlmr7TztrDk80DymPdX0IRbp9fgYuzHXyJYDEwMZ0Of7Kbx2HQSHZjDdpvOXq9QzueDgrwcNt0lcEv6mvvbhhepyu0F_oYN0HukoFlyE1OIfvE3SHmrLMBODDgefPt1pH0XgVlr_B-HpiuDzxr0sNxTyrRZTdrUcbu6_sdBtj7lWmsBXcY0mWA0d0Z4wfEfBOg_35Y05lUNtkShvO1wwlYrzA8Gk8mYUSLDOeZKnEnHMskEKOgs91f1BugpsZ5l0NwsbkuziLm3JokxMTvGNCs6ox-kqjhyXvItJqULzJiSFp-qrkIqNUAqnzE7B_JSN6kZZ8MQl0Q0bpukCb7QZ7mFhvhF760VFxKF4eIF8ZYuEM7bImEzrdB9DddTMWLhcxbeGUQo7h4Zvq4crAnbqsi5aa-WBEWMWrkvSHvEVB5-a_g9FArJD3JFBfrM76lP9mu-6CTiv4cldQiJ0x12yKmqxSXwQnr6Zqw7QVT5HZnuYgMTAuaEL0B04hYjCYF7dOc0Jo9PfWFv-wnLh9_g0cBLxrD95UdN9hTKb1Pc6SezVlpgqoYfAwwQzNulRGv0tEoifHMv0i38xWeEavPM_fO1HSgkAVOtxrHFfggar3xcKkq2Sk2Ekbq-qGrrwU1yTpnZ3Pdl4PSei2M7JetG0s7BAzrX-5zBPAWeZMsjpCGICBuCu__TWbwXz9r1Ft034llWCQ_JNQVVa4XeMXIEv4-W9qev6yZuv-qmhCM15iI-GGHWG7f9e8-b98qDTBY2Kt_txkN6RRpaa1Y1OvQ6j1TeoJU4NUeyIhPQCUnxajrB6r7IFHsS7z4jihIhM6QDq3OmGD50V-Fa60aIIG8yKNE6","summary":[{"type":"summary_text","text":"**Defining dispatch and cleanup behavior**"}]},{"id":"fc_0698befac979932d016a58ab2abd58819ab9cdbc1beac42921","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher attach replay behavior\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"300-430,700-830\"}","call_id":"call_J8B5LDyBmJwPkqtgsSxpxnpk","name":"read"},{"id":"fc_0698befac979932d016a58ab2abda4819a81d090f4cdd8a7f7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher supervisor boot\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"315-375\"}","call_id":"call_YJYlmTLUoeI9qQ26cwla3tYy","name":"read"},{"id":"fc_0698befac979932d016a58ab2abdb0819abc8fc1582f331562","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream subscribe cleanup\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3010-3210,4180-4330\"}","call_id":"call_ZfAl6CQuQ9fxAGT1spD9rVd8","name":"read"},{"id":"fc_0698befac979932d016a58ab2abdb8819a815e920dc370e847","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream ownership internals\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"80-180,640-820,1210-1435\"}","call_id":"call_r2nkOwFpwyjA71zF5pGwxoX6","name":"read"},{"id":"fc_0698befac979932d016a58ab2abdc0819aad8603c19115b2c2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc idle stall behavior\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1510-1710,1770-2010\"}","call_id":"call_rM3hgUyrT0fQ0lczENr7jSPU","name":"read"},{"id":"fc_0698befac979932d016a58ab2abe14819a8504d42e4ad92d26","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatcher restart tests\",\"pattern\":\"brain restart|restart|replay|resume|held stream|serve_attach\",\"path\":\"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/src/dispatch.rs;crates/spt/tests/brain_survive.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_lwSmY3D0I7Utx4lV2nMbeRZs","name":"grep"},{"id":"fc_0698befac979932d016a58ab2abe48819a8ac4ab86832b902f","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding refresh live rc coverage\",\"pattern\":\"daemon refresh|refresh.*rc|rc.*refresh|attach.*refresh|viewport.*brain|controller.*refresh\",\"path\":\"crates/spt/tests;crates/spt-daemon/tests;crates/spt/src/rc.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_vF1WTNJii7Y9DM7unwqTfy9n","name":"grep"}]},"duration":13648.322200000286,"ttft":5238.015300000086,"contextSnapshot":{"promptTokens":131516,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_J8B5LDyBmJwPkqtgsSxpxnpk|fc_0698befac979932d016a58ab2abd58819ab9cdbc1beac42921","toolName":"read","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher attach replay behavior"},"id":"bf07241a","parentId":"f51c39d9","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YJYlmTLUoeI9qQ26cwla3tYy|fc_0698befac979932d016a58ab2abda4819a81d090f4cdd8a7f7","toolName":"read","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading dispatcher supervisor boot"},"id":"a43cc161","parentId":"bf07241a","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZfAl6CQuQ9fxAGT1spD9rVd8|fc_0698befac979932d016a58ab2abdb0819abc8fc1582f331562","toolName":"read","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading stream subscribe cleanup"},"id":"689fcb48","parentId":"a43cc161","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_r2nkOwFpwyjA71zF5pGwxoX6|fc_0698befac979932d016a58ab2abdb8819a815e920dc370e847","toolName":"read","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream ownership internals"},"id":"9809707d","parentId":"689fcb48","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rM3hgUyrT0fQ0lczENr7jSPU|fc_0698befac979932d016a58ab2abdc0819aad8603c19115b2c2","toolName":"read","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc idle stall behavior"},"id":"73a01786","parentId":"9809707d","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lwSmY3D0I7Utx4lV2nMbeRZs|fc_0698befac979932d016a58ab2abe14819a8504d42e4ad92d26","toolName":"grep","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/src/dispatch.rs;crates/spt/tests/brain_survive.rs"},"intent":"Finding dispatcher restart tests"},"id":"814374f0","parentId":"73a01786","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vF1WTNJii7Y9DM7unwqTfy9n|fc_0698befac979932d016a58ab2abe48819a8ac4ab86832b902f","toolName":"grep","startedAt":"2026-07-16T09:58:03.337Z","args":{"path":"crates/spt/tests;crates/spt-daemon/tests;crates/spt/src/rc.rs"},"intent":"Finding refresh live rc coverage"},"id":"ab7696b9","parentId":"814374f0","timestamp":"2026-07-16T09:58:03.337Z"}
{"type":"message","id":"f089ef2c","parentId":"ab7696b9","timestamp":"2026-07-16T09:58:03.357Z","message":{"role":"toolResult","toolCallId":"call_r2nkOwFpwyjA71zF5pGwxoX6|fc_0698befac979932d016a58ab2abdb8819a815e920dc370e847","toolName":"read","content":[{"type":"text","text":"// [impl->REQ-PUMP-PEER-ISOLATION]\nconst PUMP_DIAL_CONCURRENCY: usize = 24;\n\n/// Everything needed to stand the broker's endpoint up: the node's identity\n/// (the endpoint *is* the node's name — EndpointId == pubkey), the relay stance\n/// (from `daemon.json` in production; [`RelayPolicy::Disabled`] in hermetic\n/// tests), and whether mDNS LAN discovery rides along.\npub struct NetConfig {\n    /// The node identity the endpoint binds as (seed → same ed25519 keypair).\n    pub identity: Identity,\n    /// Relay stance (ADR-0002): n0 default / self-host / disabled.\n    pub relay: RelayPolicy,\n    /// mDNS LAN discovery on/off (off in hermetic tests).\n    pub discovery: LocalDiscovery,\n    /// Socket bind scope: [`BindScope::All`] in production;\n    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,\n    /// no per-test-binary Windows firewall prompt).\n    pub scope: BindScope,\n    /// Live source of this node's subnet credentials for the connect-time\n    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at\n    /// every dial + accept (fail → drop); `None` disables it entirely — both\n    /// peers must agree, or the side expecting the control stream would hang.\n    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that\n    /// don't exercise membership pass `None`.\n    pub membership: Option<MembershipSource>,\n}\n\n/// Default bounded stream-read ring depth (chunks) — same sizing rationale as\n/// the PTY `OutputLog`: large enough that a brain-restart window never evicts\n/// unreplayed chunks in practice; bounded so an unsubscribed stream can't\n/// buffer without limit.\nconst DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n\n/// Max bytes pulled per stream read (one ring chunk).\nconst STREAM_READ_CHUNK: usize = 64 * 1024;\n\n/// Transport backing a held connection. A real `Quic` connection (the only\n/// kind an inbound handshake can produce), or a broker-minted `Loopback` —\n/// an in-process connection with NO iroh handshake whose identity IS this node,\n/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side\n/// via [`NetHost::dial_loopback`]; it can never arise from an inbound handshake\n/// (a remote always presents its own key and cannot claim to be us). This is\n/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12\n/// W1.5, B1) — QUIC self-dial is refused by iroh (\"Connecting to ourself is not\n/// supported\"), so local attach rides a broker loopback conn instead.\nenum ConnKind {\n    /// A real QUIC connection — kept alive by ownership here; dropping it closes\n    /// the connection. Cloned (cheap handle) for stream opens.\n    Quic(Connection),\n    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its\n    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];\n    /// there is no accept loop and no closed-watcher (it lives for the host).\n    Loopback,\n}\n\n/// One live connection the broker holds (QUIC or loopback).\nstruct ConnEntry {\n    /// The transport backing this connection.\n    kind: ConnKind,\n    /// The remote node's stable name (hex pubkey). For QUIC, proven by the\n    /// handshake; for loopback, THIS node's own id (the broker-minted identity).\n    remote_id_hex: String,\n    /// The subnets the peer proved current-epoch seed-knowledge of at connect\n    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:\n    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;\n    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run\n    /// in parallel — belt + suspenders, no regression).\n    proven_subnets: HashSet<String>,\n}\n\n/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`\n/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n…\n}\n…\nfn register_stream(\n…\n    conn_id: u64,\n    remote_id_hex: &str,\n    send: SendHalf,\n    recv: RecvHalf,\n    initiated_locally: bool,\n) -> u64 {\n    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n    let room = Arc::new(tokio::sync::Notify::new());\n    shared.streams.lock().unwrap().insert(\n        id,\n        Arc::new(StreamEntry {\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            initiated_locally,\n            send: tokio::sync::Mutex::new(send),\n            log: Arc::clone(&log),\n            room: Arc::clone(&room),\n        }),\n    );\n\n    // The read pump — the stream's single producer. A clean end (peer finish)\n    // and a torn end (conn lost) both finish the log; the buffered chunks stay\n    // replayable either way. The transport differs only at this read leaf\n    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the\n    // same `StreamLog`, so nothing above the pump can tell them apart.\n    //\n    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n    // so before pulling the next chunk the pump WAITS while the undrained ring is\n    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n    tokio::spawn(async move {\n        match recv {\n            RecvHalf::Quic(mut recv) => loop {\n                await_ring_room(&log, &room).await;\n                match recv.read_chunk(STREAM_READ_CHUNK).await {\n                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n                    Ok(None) | Err(_) => {\n                        log.lock().unwrap().finish();\n                        break;\n                    }\n                }\n            },\n            RecvHalf::Loopback(mut recv) => {\n                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n                loop {\n                    await_ring_room(&log, &room).await;\n                    match recv.read(&mut buf).await {\n                        Ok(0) | Err(_) => {\n                            log.lock().unwrap().finish();\n                            break;\n                        }\n                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n                    }\n                }\n            }\n        }\n    });\n\n    id\n}\n\n/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.\n// [impl->REQ-SHELL-4]\nasync fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n    loop {\n        {\n            let l = log.lock().unwrap();\n            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n                return;\n            }\n        }\n        room.notified().await;\n    }\n}\n\n/// The broker-owned net endpoint + live connection/stream tables (see module\n/// docs).\npub struct NetHost {\n    /// The dedicated tokio runtime hosting the endpoint and its accept loop.\n    /// Dropping the host drops the runtime, stopping every net task with it.\n    runtime: tokio::runtime::Runtime,\n    endpoint: Arc<NetEndpoint>,\n    shared: Arc<NetShared>,\n    /// `(minter, op_id) → conn_id` for exactly-once dial replies: a deduped replay\n    /// of a journaled dial is answered with the connection the first apply opened.\n    /// Keyed by `(minter, op_id)` (ADR-0034) — the minter namespaces the op so two\n    /// counters that both start at 1 cannot clobber each other's dial record.\n    dial_ops: Mutex<HashMap<(Minter, u64), u64>>,\n    /// `(minter, op_id) → stream_id` for exactly-once stream-open replies (D4b),\n    /// same discipline as `dial_ops`. Minter-namespaced (ADR-0034), matching the\n    /// journal key the broker builds for the same op.\n    stream_ops: Mutex<HashMap<(Minter, u64), u64>>,\n    /// This node's pubkey (the seed-proof's local channel-binding identity).\n    local_id: PublicKey,\n    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables\n    /// the proof (see [`NetConfig::membership`]).\n    membership: Option<MembershipSource>,\n    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live\n    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`\n    /// so the many hermetic test configs need no change — a roster exchange is a\n    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +\n    /// cloned per connect on the net runtime, so the accept loop (spawned in\n    /// `start`, before the host exists) shares it through this `Arc`.\n    roster: Arc<Mutex<Option<RosterExchange>>>,\n    /// The broker's single in-process loopback connection (M12 W1.5, B1),\n    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter\n    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the\n    /// row lives for the host (no handshake to lose, no closed-watcher).\n    loopback_conn: Mutex<Option<u64>>,\n    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery\n    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the\n    /// one row this open minted — concurrent pair-opens on the shared loopback conn\n    /// would otherwise race the diff.\n    loopback_pair_lock: Mutex<()>,\n    /// The broker-side QUIC-op deadline in ms (REQ-HAZARD-BROKER-QUIC-DEADLINE),\n    /// default [`BROKER_QUIC_OP_TIMEOUT_MS`]; the hermetic black-hole tests lower\n    /// it via [`Self::set_quic_op_timeout`]. Interior-mutable so the setter takes\n    /// `&self`, like the live host's other post-`start` knobs (`set_roster_exchange`).\n    quic_op_timeout_ms: AtomicU64,\n    /// The bounded concurrent-dial cap (PUMP-TRUTH W2, G4): a shared\n    /// [`tokio::sync::Semaphore`] cap [`PUMP_DIAL_CONCURRENCY`]. BOTH the\n    /// non-blocking [`Self::submit_dial`] AND the blocking [`Self::dial`] acquire\n    /// a permit from THIS one, so the cap bounds every dial source (Q2 caveat).\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    dial_semaphore: Arc<tokio::sync::Semaphore>,\n    /// DIAGNOSTIC (UPDATE-WEDGE round-3, throwaway — NOT a shipped feature):\n    /// the count of in-flight [`Self::submit_dial`] tasks currently occupying\n    /// the 2-worker net runtime. A dial task increments on entry, decrements on\n    /// completion (RAII, panic-safe). The round-3 starvation rig reads this to\n    /// size a dead-peer burst against the worker pool.\n    active_dial_tasks: Arc<AtomicUsize>,\n    /// DIAGNOSTIC (UPDATE-WEDGE round-3, throwaway): reactor-liveness canary. A\n    /// runtime task stamps epoch-ms here every ~25ms. If the 2 workers are\n    /// MONOPOLIZED (a non-yielding dead-peer connect that never awaits back to\n    /// the scheduler) the stamp goes STALE — the DIRECT worker-starvation\n    /// discriminator the round-3 rig asserts on. A fresh stamp under a dead-peer\n    /// burst PROVES the QUIC awaits yield cleanly (⇒ the 2-worker-starvation\n    /// seam is FALSIFIED and must be re-opened, per the round-3 ruling §5).\n    net_canary_tick_ms: Arc<AtomicU64>,\n}\n\n/// DIAGNOSTIC helper (round-3): epoch-ms wall clock for the canary stamp.\nfn epoch_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/// DIAGNOSTIC (round-3): decrement the in-flight dial gauge on drop, so a dial\n/// task that panics or is cancelled still leaves the count correct.\nstruct DialGauge(Arc<AtomicUsize>);\nimpl Drop for DialGauge {\n    fn drop(&mut self) {\n        self.0.fetch_sub(1, Ordering::Relaxed);\n    }\n}\n\nimpl NetHost {\n    /// Bind the broker's endpoint on a fresh dedicated runtime and start the\n    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the\n    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and\n    /// the pre-trust pairing ALPN — routed to the always-on responder\n    /// ([`crate::pairhost::respond`]), never registered in the stream table.\n    /// One endpoint for both is deliberate: the pairing transcript binds and\n    /// pins the REAL node pubkey, and the relay can only route one endpoint\n    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).\n    /// The meet-rotation listener loop rides the same runtime.\n    // [impl->REQ-DAEMON-2]\n    // [impl->REQ-SUBNET-2]\n    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {\n        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the\n        // \"no network reached\" promise — the ceremony clock's lazy NTP query\n        // must honor it (tests must never probe a real NTP server). Real\n        // scopes keep the decision-18 offset correction.\n        spt_net::net::pairing::ntp::set_enabled(!matches!(cfg.scope, BindScope::Loopback));\n…\n    }\n…\n    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {\n…\n    }\n\n    /// The OBSERVED peer address of a held connection (remote id + the\n    /// connection's current path addresses) as opaque JSON — the address-\n    /// seeding write-back the `net-dialed` reply carries (M8 decision 20,\n    /// REQ-CONV-1). `null` when the conn is gone, path-less, or fails to\n    /// serialize: the seed is an optimization, never an error path.\n    // [impl->REQ-CONV-1]\n    pub fn conn_remote_addr_json(&self, conn_id: u64) -> serde_json::Value {\n        let conn = {\n            let conns = self.shared.conns.lock().unwrap();\n            match conns.get(&conn_id) {\n                // A loopback conn has no path to observe — nothing to seed.\n                Some(e) => match &e.kind {\n                    ConnKind::Quic(c) => c.clone(),\n                    ConnKind::Loopback => return serde_json::Value::Null,\n                },\n                None => return serde_json::Value::Null,\n            }\n        };\n        observed_addr_json(&conn)\n    }\n\n    /// Mint (or reuse) the broker's single in-process loopback connection\n    /// (M12 W1.5, B1) and return `(conn_id, remote_id_hex)` — `remote_id_hex` is\n    /// THIS node's own id, so a stream served over it gates same-node (access\n    /// short-circuit Allow). The conn carries NO iroh handshake: its identity is\n    /// broker-minted BY CONSTRUCTION and can never arise from an inbound dial.\n    /// This is what lets a LOCAL `spt rc` ride the byte-identical `serve_attach`\n    /// pump — QUIC self-dial is refused by iroh, so local attach uses this.\n    /// Idempotent: every call returns the same singleton conn (one is all a\n    /// local attach needs); no journaling needed (reuse is the dedup).\n    // [impl->REQ-RC-1]\n    pub fn dial_loopback(&self) -> (u64, String) {\n        let remote_id_hex = self.local_id.to_hex();\n        let mut slot = self.loopback_conn.lock().unwrap();\n        if let Some(id) = *slot {\n            return (id, remote_id_hex);\n        }\n        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n        self.shared.conns.lock().unwrap().insert(\n            id,\n            ConnEntry {\n                kind: ConnKind::Loopback,\n                remote_id_hex: remote_id_hex.clone(),\n                proven_subnets: HashSet::new(),\n            },\n        );\n        *slot = Some(id);\n        (id, remote_id_hex)\n    }\n\n    /// Open a bidi stream on a held connection, registering it (with its read\n    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer\n    /// sees the stream on its first write.\n    ///\n    /// On a [`ConnKind::Loopback`] conn there is no peer to accept the stream,\n    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row\n    /// (`initiated_locally = true`, the returned id) and a peer row\n    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via\n    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened\n    /// stream. One `tokio::io::duplex` wires both directions; each row's read\n    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5\n    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,\n    /// so its serve gates same-node.\n    // [impl->REQ-DAEMON-2]\n    // [impl->REQ-RC-1]\n    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {\n        enum Backing {\n            Quic(Connection),\n            Loopback,\n        }\n        let (backing, remote_id_hex) = {\n            let conns = self.shared.conns.lock().unwrap();\n            let e = conns.get(&conn_id).ok_or_else(|| {\n                io::Error::new(io::ErrorKind::NotFound, format!(\"no such conn {conn_id}\"))\n            })?;\n            let backing = match &e.kind {\n                ConnKind::Quic(c) => Backing::Quic(c.clone()),\n                ConnKind::Loopback => Backing::Loopback,\n            };\n            (backing, e.remote_id_hex.clone())\n        };\n        let shared = Arc::clone(&self.shared);\n        match backing {\n            // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `open_bi` on a black-holed\n            // peer can block on stream flow-control credit the dead peer never grants.\n            Backing::Quic(conn) => self.bounded_block_on(async move {\n                let (send, recv) = conn\n                    .open_bi()\n                    .await\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Quic(send),\n                    RecvHalf::Quic(recv),\n                    true,\n                ))\n            }),\n            // Bounded like the QUIC arm (REQ-BROKER-ATTACH-JOURNAL-RESILIENT, bug\n            // #16): the loopback open is all in-memory and normally returns\n            // instantly, but a SICK NetHost runtime (saturated/wedged after a\n            // self-update brain-respawn) would otherwise hang this journaled op with\n            // no deadline — killing the per-conn reply thread so every attach\n            // silently deadlines (\"brain IPC read deadline elapsed\") while\n            // non-journaled ops keep working. `bounded_block_on` turns that into a\n            // prompt ordinary error frame; the bound never bites a healthy runtime.\n            // [impl->REQ-BROKER-ATTACH-JOURNAL-RESILIENT]\n            Backing::Loopback => self.bounded_block_on(async move {\n                // One duplex carries BOTH directions: operator writes reach the\n                // peer's read pump and vice-versa. Split each end into a read +\n                // write half so each row owns exactly its leg.\n                let (op_io, peer_io) = tokio::io::duplex(STREAM_READ_CHUNK);\n                let (op_r, op_w) = tokio::io::split(op_io);\n                let (peer_r, peer_w) = tokio::io::split(peer_io);\n                // Register the peer (inbound) row first so it is in the table\n                // before the operator can write its Request onto the wire — the\n                // dispatcher must never miss the stream.\n                register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(peer_w),\n                    RecvHalf::Loopback(peer_r),\n                    false,\n                );\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(op_w),\n                    RecvHalf::Loopback(op_r),\n                    true,\n                ))\n            }),\n        }\n    }\n\n    /// Record that journaled open `(minter, op_id)` minted `stream_id` (inside the\n    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).\n    pub fn record_stream_op(&self, minter: Minter, op_id: u64, stream_id: u64) {\n        self.stream_ops\n            .lock()\n            .unwrap()\n            .insert((minter, op_id), stream_id);\n    }\n\n    /// The stream a journaled open `(minter, op_id)` minted, if still held.\n    pub fn stream_op_id(&self, minter: Minter, op_id: u64) -> Option<u64> {\n        self.stream_ops\n            .lock()\n            .unwrap()\n            .get(&(minter, op_id))\n            .copied()\n    }\n\n    /// A held stream's `(conn_id, remote_id_hex)`.\n    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {\n        self.shared\n            .streams\n            .lock()\n            .unwrap()\n            .get(&stream_id)\n            .map(|e| (e.conn_id, e.remote_id_hex.clone()))\n    }\n\n    /// Write `bytes` to a held stream's send side; `finish` closes the send\n    /// 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        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {\n            e.log.lock().unwrap().set_retentive(cap);\n        }\n    }\n…\n}"}],"details":{"displayContent":{"text":"// [impl->REQ-PUMP-PEER-ISOLATION]\nconst PUMP_DIAL_CONCURRENCY: usize = 24;\n\n/// Everything needed to stand the broker's endpoint up: the node's identity\n/// (the endpoint *is* the node's name — EndpointId == pubkey), the relay stance\n/// (from `daemon.json` in production; [`RelayPolicy::Disabled`] in hermetic\n/// tests), and whether mDNS LAN discovery rides along.\npub struct NetConfig {\n    /// The node identity the endpoint binds as (seed → same ed25519 keypair).\n    pub identity: Identity,\n    /// Relay stance (ADR-0002): n0 default / self-host / disabled.\n    pub relay: RelayPolicy,\n    /// mDNS LAN discovery on/off (off in hermetic tests).\n    pub discovery: LocalDiscovery,\n    /// Socket bind scope: [`BindScope::All`] in production;\n    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,\n    /// no per-test-binary Windows firewall prompt).\n    pub scope: BindScope,\n    /// Live source of this node's subnet credentials for the connect-time\n    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at\n    /// every dial + accept (fail → drop); `None` disables it entirely — both\n    /// peers must agree, or the side expecting the control stream would hang.\n    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that\n    /// don't exercise membership pass `None`.\n    pub membership: Option<MembershipSource>,\n}\n\n/// Default bounded stream-read ring depth (chunks) — same sizing rationale as\n/// the PTY `OutputLog`: large enough that a brain-restart window never evicts\n/// unreplayed chunks in practice; bounded so an unsubscribed stream can't\n/// buffer without limit.\nconst DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n\n/// Max bytes pulled per stream read (one ring chunk).\nconst STREAM_READ_CHUNK: usize = 64 * 1024;\n\n/// Transport backing a held connection. A real `Quic` connection (the only\n/// kind an inbound handshake can produce), or a broker-minted `Loopback` —\n/// an in-process connection with NO iroh handshake whose identity IS this node,\n/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side\n/// via [`NetHost::dial_loopback`]; it can never arise from an inbound handshake\n/// (a remote always presents its own key and cannot claim to be us). This is\n/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12\n/// W1.5, B1) — QUIC self-dial is refused by iroh (\"Connecting to ourself is not\n/// supported\"), so local attach rides a broker loopback conn instead.\nenum ConnKind {\n    /// A real QUIC connection — kept alive by ownership here; dropping it closes\n    /// the connection. Cloned (cheap handle) for stream opens.\n    Quic(Connection),\n    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its\n    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];\n    /// there is no accept loop and no closed-watcher (it lives for the host).\n    Loopback,\n}\n\n/// One live connection the broker holds (QUIC or loopback).\nstruct ConnEntry {\n    /// The transport backing this connection.\n    kind: ConnKind,\n    /// The remote node's stable name (hex pubkey). For QUIC, proven by the\n    /// handshake; for loopback, THIS node's own id (the broker-minted identity).\n    remote_id_hex: String,\n    /// The subnets the peer proved current-epoch seed-knowledge of at connect\n    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:\n    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;\n    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run\n    /// in parallel — belt + suspenders, no regression).\n    proven_subnets: HashSet<String>,\n}\n\n/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`\n/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n…\n}\n…\nfn register_stream(\n…\n    conn_id: u64,\n    remote_id_hex: &str,\n    send: SendHalf,\n    recv: RecvHalf,\n    initiated_locally: bool,\n) -> u64 {\n    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n    let room = Arc::new(tokio::sync::Notify::new());\n    shared.streams.lock().unwrap().insert(\n        id,\n        Arc::new(StreamEntry {\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            initiated_locally,\n            send: tokio::sync::Mutex::new(send),\n            log: Arc::clone(&log),\n            room: Arc::clone(&room),\n        }),\n    );\n\n    // The read pump — the stream's single producer. A clean end (peer finish)\n    // and a torn end (conn lost) both finish the log; the buffered chunks stay\n    // replayable either way. The transport differs only at this read leaf\n    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the\n    // same `StreamLog`, so nothing above the pump can tell them apart.\n    //\n    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n    // so before pulling the next chunk the pump WAITS while the undrained ring is\n    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n    tokio::spawn(async move {\n        match recv {\n            RecvHalf::Quic(mut recv) => loop {\n                await_ring_room(&log, &room).await;\n                match recv.read_chunk(STREAM_READ_CHUNK).await {\n                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n                    Ok(None) | Err(_) => {\n                        log.lock().unwrap().finish();\n                        break;\n                    }\n                }\n            },\n            RecvHalf::Loopback(mut recv) => {\n                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n                loop {\n                    await_ring_room(&log, &room).await;\n                    match recv.read(&mut buf).await {\n                        Ok(0) | Err(_) => {\n                            log.lock().unwrap().finish();\n                            break;\n                        }\n                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n                    }\n                }\n            }\n        }\n    });\n\n    id\n}\n\n/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.\n// [impl->REQ-SHELL-4]\nasync fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n    loop {\n        {\n            let l = log.lock().unwrap();\n            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n                return;\n            }\n        }\n        room.notified().await;\n    }\n}\n\n/// The broker-owned net endpoint + live connection/stream tables (see module\n/// docs).\npub struct NetHost {\n    /// The dedicated tokio runtime hosting the endpoint and its accept loop.\n    /// Dropping the host drops the runtime, stopping every net task with it.\n    runtime: tokio::runtime::Runtime,\n    endpoint: Arc<NetEndpoint>,\n    shared: Arc<NetShared>,\n    /// `(minter, op_id) → conn_id` for exactly-once dial replies: a deduped replay\n    /// of a journaled dial is answered with the connection the first apply opened.\n    /// Keyed by `(minter, op_id)` (ADR-0034) — the minter namespaces the op so two\n    /// counters that both start at 1 cannot clobber each other's dial record.\n    dial_ops: Mutex<HashMap<(Minter, u64), u64>>,\n    /// `(minter, op_id) → stream_id` for exactly-once stream-open replies (D4b),\n    /// same discipline as `dial_ops`. Minter-namespaced (ADR-0034), matching the\n    /// journal key the broker builds for the same op.\n    stream_ops: Mutex<HashMap<(Minter, u64), u64>>,\n    /// This node's pubkey (the seed-proof's local channel-binding identity).\n    local_id: PublicKey,\n    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables\n    /// the proof (see [`NetConfig::membership`]).\n    membership: Option<MembershipSource>,\n    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live\n    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`\n    /// so the many hermetic test configs need no change — a roster exchange is a\n    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +\n    /// cloned per connect on the net runtime, so the accept loop (spawned in\n    /// `start`, before the host exists) shares it through this `Arc`.\n    roster: Arc<Mutex<Option<RosterExchange>>>,\n    /// The broker's single in-process loopback connection (M12 W1.5, B1),\n    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter\n    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the\n    /// row lives for the host (no handshake to lose, no closed-watcher).\n    loopback_conn: Mutex<Option<u64>>,\n    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery\n    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the\n    /// one row this open minted — concurrent pair-opens on the shared loopback conn\n    /// would otherwise race the diff.\n    loopback_pair_lock: Mutex<()>,\n    /// The broker-side QUIC-op deadline in ms (REQ-HAZARD-BROKER-QUIC-DEADLINE),\n    /// default [`BROKER_QUIC_OP_TIMEOUT_MS`]; the hermetic black-hole tests lower\n    /// it via [`Self::set_quic_op_timeout`]. Interior-mutable so the setter takes\n    /// `&self`, like the live host's other post-`start` knobs (`set_roster_exchange`).\n    quic_op_timeout_ms: AtomicU64,\n    /// The bounded concurrent-dial cap (PUMP-TRUTH W2, G4): a shared\n    /// [`tokio::sync::Semaphore`] cap [`PUMP_DIAL_CONCURRENCY`]. BOTH the\n    /// non-blocking [`Self::submit_dial`] AND the blocking [`Self::dial`] acquire\n    /// a permit from THIS one, so the cap bounds every dial source (Q2 caveat).\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    dial_semaphore: Arc<tokio::sync::Semaphore>,\n    /// DIAGNOSTIC (UPDATE-WEDGE round-3, throwaway — NOT a shipped feature):\n    /// the count of in-flight [`Self::submit_dial`] tasks currently occupying\n    /// the 2-worker net runtime. A dial task increments on entry, decrements on\n    /// completion (RAII, panic-safe). The round-3 starvation rig reads this to\n    /// size a dead-peer burst against the worker pool.\n    active_dial_tasks: Arc<AtomicUsize>,\n    /// DIAGNOSTIC (UPDATE-WEDGE round-3, throwaway): reactor-liveness canary. A\n    /// runtime task stamps epoch-ms here every ~25ms. If the 2 workers are\n    /// MONOPOLIZED (a non-yielding dead-peer connect that never awaits back to\n    /// the scheduler) the stamp goes STALE — the DIRECT worker-starvation\n    /// discriminator the round-3 rig asserts on. A fresh stamp under a dead-peer\n    /// burst PROVES the QUIC awaits yield cleanly (⇒ the 2-worker-starvation\n    /// seam is FALSIFIED and must be re-opened, per the round-3 ruling §5).\n    net_canary_tick_ms: Arc<AtomicU64>,\n}\n\n/// DIAGNOSTIC helper (round-3): epoch-ms wall clock for the canary stamp.\nfn epoch_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/// DIAGNOSTIC (round-3): decrement the in-flight dial gauge on drop, so a dial\n/// task that panics or is cancelled still leaves the count correct.\nstruct DialGauge(Arc<AtomicUsize>);\nimpl Drop for DialGauge {\n    fn drop(&mut self) {\n        self.0.fetch_sub(1, Ordering::Relaxed);\n    }\n}\n\nimpl NetHost {\n    /// Bind the broker's endpoint on a fresh dedicated runtime and start the\n    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the\n    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and\n    /// the pre-trust pairing ALPN — routed to the always-on responder\n    /// ([`crate::pairhost::respond`]), never registered in the stream table.\n    /// One endpoint for both is deliberate: the pairing transcript binds and\n    /// pins the REAL node pubkey, and the relay can only route one endpoint\n    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).\n    /// The meet-rotation listener loop rides the same runtime.\n    // [impl->REQ-DAEMON-2]\n    // [impl->REQ-SUBNET-2]\n    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {\n        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the\n        // \"no network reached\" promise — the ceremony clock's lazy NTP query\n        // must honor it (tests must never probe a real NTP server). Real\n        // scopes keep the decision-18 offset correction.\n        spt_net::net::pairing::ntp::set_enabled(!matches!(cfg.scope, BindScope::Loopback));\n…\n    }\n…\n    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {\n…\n    }\n\n    /// The OBSERVED peer address of a held connection (remote id + the\n    /// connection's current path addresses) as opaque JSON — the address-\n    /// seeding write-back the `net-dialed` reply carries (M8 decision 20,\n    /// REQ-CONV-1). `null` when the conn is gone, path-less, or fails to\n    /// serialize: the seed is an optimization, never an error path.\n    // [impl->REQ-CONV-1]\n    pub fn conn_remote_addr_json(&self, conn_id: u64) -> serde_json::Value {\n        let conn = {\n            let conns = self.shared.conns.lock().unwrap();\n            match conns.get(&conn_id) {\n                // A loopback conn has no path to observe — nothing to seed.\n                Some(e) => match &e.kind {\n                    ConnKind::Quic(c) => c.clone(),\n                    ConnKind::Loopback => return serde_json::Value::Null,\n                },\n                None => return serde_json::Value::Null,\n            }\n        };\n        observed_addr_json(&conn)\n    }\n\n    /// Mint (or reuse) the broker's single in-process loopback connection\n    /// (M12 W1.5, B1) and return `(conn_id, remote_id_hex)` — `remote_id_hex` is\n    /// THIS node's own id, so a stream served over it gates same-node (access\n    /// short-circuit Allow). The conn carries NO iroh handshake: its identity is\n    /// broker-minted BY CONSTRUCTION and can never arise from an inbound dial.\n    /// This is what lets a LOCAL `spt rc` ride the byte-identical `serve_attach`\n    /// pump — QUIC self-dial is refused by iroh, so local attach uses this.\n    /// Idempotent: every call returns the same singleton conn (one is all a\n    /// local attach needs); no journaling needed (reuse is the dedup).\n    // [impl->REQ-RC-1]\n    pub fn dial_loopback(&self) -> (u64, String) {\n        let remote_id_hex = self.local_id.to_hex();\n        let mut slot = self.loopback_conn.lock().unwrap();\n        if let Some(id) = *slot {\n            return (id, remote_id_hex);\n        }\n        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n        self.shared.conns.lock().unwrap().insert(\n            id,\n            ConnEntry {\n                kind: ConnKind::Loopback,\n                remote_id_hex: remote_id_hex.clone(),\n                proven_subnets: HashSet::new(),\n            },\n        );\n        *slot = Some(id);\n        (id, remote_id_hex)\n    }\n\n    /// Open a bidi stream on a held connection, registering it (with its read\n    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer\n    /// sees the stream on its first write.\n    ///\n    /// On a [`ConnKind::Loopback`] conn there is no peer to accept the stream,\n    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row\n    /// (`initiated_locally = true`, the returned id) and a peer row\n    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via\n    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened\n    /// stream. One `tokio::io::duplex` wires both directions; each row's read\n    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5\n    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,\n    /// so its serve gates same-node.\n    // [impl->REQ-DAEMON-2]\n    // [impl->REQ-RC-1]\n    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {\n        enum Backing {\n            Quic(Connection),\n            Loopback,\n        }\n        let (backing, remote_id_hex) = {\n            let conns = self.shared.conns.lock().unwrap();\n            let e = conns.get(&conn_id).ok_or_else(|| {\n                io::Error::new(io::ErrorKind::NotFound, format!(\"no such conn {conn_id}\"))\n            })?;\n            let backing = match &e.kind {\n                ConnKind::Quic(c) => Backing::Quic(c.clone()),\n                ConnKind::Loopback => Backing::Loopback,\n            };\n            (backing, e.remote_id_hex.clone())\n        };\n        let shared = Arc::clone(&self.shared);\n        match backing {\n            // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `open_bi` on a black-holed\n            // peer can block on stream flow-control credit the dead peer never grants.\n            Backing::Quic(conn) => self.bounded_block_on(async move {\n                let (send, recv) = conn\n                    .open_bi()\n                    .await\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Quic(send),\n                    RecvHalf::Quic(recv),\n                    true,\n                ))\n            }),\n            // Bounded like the QUIC arm (REQ-BROKER-ATTACH-JOURNAL-RESILIENT, bug\n            // #16): the loopback open is all in-memory and normally returns\n            // instantly, but a SICK NetHost runtime (saturated/wedged after a\n            // self-update brain-respawn) would otherwise hang this journaled op with\n            // no deadline — killing the per-conn reply thread so every attach\n            // silently deadlines (\"brain IPC read deadline elapsed\") while\n            // non-journaled ops keep working. `bounded_block_on` turns that into a\n            // prompt ordinary error frame; the bound never bites a healthy runtime.\n            // [impl->REQ-BROKER-ATTACH-JOURNAL-RESILIENT]\n            Backing::Loopback => self.bounded_block_on(async move {\n                // One duplex carries BOTH directions: operator writes reach the\n                // peer's read pump and vice-versa. Split each end into a read +\n                // write half so each row owns exactly its leg.\n                let (op_io, peer_io) = tokio::io::duplex(STREAM_READ_CHUNK);\n                let (op_r, op_w) = tokio::io::split(op_io);\n                let (peer_r, peer_w) = tokio::io::split(peer_io);\n                // Register the peer (inbound) row first so it is in the table\n                // before the operator can write its Request onto the wire — the\n                // dispatcher must never miss the stream.\n                register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(peer_w),\n                    RecvHalf::Loopback(peer_r),\n                    false,\n                );\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(op_w),\n                    RecvHalf::Loopback(op_r),\n                    true,\n                ))\n            }),\n        }\n    }\n\n    /// Record that journaled open `(minter, op_id)` minted `stream_id` (inside the\n    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).\n    pub fn record_stream_op(&self, minter: Minter, op_id: u64, stream_id: u64) {\n        self.stream_ops\n            .lock()\n            .unwrap()\n            .insert((minter, op_id), stream_id);\n    }\n\n    /// The stream a journaled open `(minter, op_id)` minted, if still held.\n    pub fn stream_op_id(&self, minter: Minter, op_id: u64) -> Option<u64> {\n        self.stream_ops\n            .lock()\n            .unwrap()\n            .get(&(minter, op_id))\n            .copied()\n    }\n\n    /// A held stream's `(conn_id, remote_id_hex)`.\n    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {\n        self.shared\n            .streams\n            .lock()\n            .unwrap()\n            .get(&stream_id)\n            .map(|e| (e.conn_id, e.remote_id_hex.clone()))\n    }\n\n    /// Write `bytes` to a held stream's send side; `finish` closes the send\n    /// 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        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {\n            e.log.lock().unwrap().set_retentive(cap);\n        }\n    }\n…\n}","startLine":80,"lineNumbers":[80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,null,186,null,638,null,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,null,948,null,1202,null,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,null,1557]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784195883357}}
{"type":"message","id":"faa3f8f9","parentId":"f089ef2c","timestamp":"2026-07-16T09:58:03.400Z","message":{"role":"toolResult","toolCallId":"call_J8B5LDyBmJwPkqtgsSxpxnpk|fc_0698befac979932d016a58ab2abd58819ab9cdbc1beac42921","toolName":"read","content":[{"type":"text","text":"    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…\n            }\n        }\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":"    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…\n            }\n        }\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","startLine":299,"lineNumbers":[299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,null,432,433,null,456,null,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784195883400}}
{"type":"message","id":"3c3fbb55","parentId":"faa3f8f9","timestamp":"2026-07-16T09:58:03.400Z","message":{"role":"toolResult","toolCallId":"call_ZfAl6CQuQ9fxAGT1spD9rVd8|fc_0698befac979932d016a58ab2abdb0819abc8fc1582f331562","toolName":"read","content":[{"type":"text","text":"        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_INPUT => {\n                    if let Err(msg) = self.dispatch_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_KILL => {\n                    if let Err(msg) = self.dispatch_kill(env) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\n                KIND_ADAPTER_APPLY => {\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\n                KIND_NET_DIAL => {\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_DIAL_LOOPBACK => {\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-PUMP-PEER-ISOLATION]\n                KIND_NET_DIAL_SUBMIT => {\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_JOIN => {\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_MEET => {\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_CODE_SUBMIT => {\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPEN => {\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SEND => {\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAMS => {\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_SESSIONS => {\n                    // Clone the (id, endpoint, log) out from under the sessions\n                    // lock, then read each log's cursor under the log lock alone —\n                    // never both locks at once (the dispatch_subscribe discipline).\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n                        let sessions = recover(&self.sessions);\n                        sessions\n                            .iter()\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\n                            .collect()\n                    };\n                    // Snapshot each session's truth under its OWN log lock (cursor +\n                    // controller identity + the control/viewer stamp truth), doing NO\n                    // file I/O under the lock (the lock-across-effect discipline, KH\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\n                    // the lock.\n                    struct SessSnap {\n                        id: u64,\n                        endpoint: String,\n                        resume_seq: u64,\n                        controller_by: Option<String>,\n                        has_controller: bool,\n                        viewers: u32,\n                    }\n                    let snaps: Vec<SessSnap> = rows\n                        .into_iter()\n                        .map(|(id, endpoint, log)| {\n                            let mut log = recover_log(&log);\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\n                            // truth HONEST before reading it — reap a dead-writer\n                            // (severed) controller so both the reply's controller_by\n                            // AND the off-lock converge_perch_stamps below see the\n                            // cleared state and the stale info.json stamp clears.\n                            let _ = log.reap_dead_controller();\n                            SessSnap {\n                                id,\n                                endpoint,\n                                resume_seq: log.delivered_through(),\n                                controller_by: log.controller_by().map(str::to_string),\n                                has_controller: log.has_controller(),\n                                viewers: log.live_viewer_count(),\n                            }\n                        })\n                        .collect();\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        let sessions = recover(&self.sessions);\n        for id in &my_subs {\n            if let Some(h) = sessions.get(id) {\n                recover_log(&h.log).detach_if(&send);\n            }\n        }\n        drop(sessions);\n        // Same for net streams: the streams + their rings persist (D4b).\n        if let Some(host) = self.net.get() {\n            for id in &my_stream_subs {\n                host.detach_stream_sub(*id, &send);\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n…\n        }\n…\n    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\n…\n            Err(fail) => {\n                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony\n                // against the same held address); every other verdict is terminal.\n                if !fail_keeps_hold(&fail) {\n                    recover(&self.pair_holds).remove(&req.session_id);\n                }\n                pair_fail_frame(&fail)\n            }\n        };\n        send_frame(send, &frame);\n        Ok(())\n    }\n\n    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once\n    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted\n    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped\n    /// replay is answered with the stream the first apply opened.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        let build = |stream_id: u64, conn_id: u64, op_id: Option<u64>, applied_now: bool| {\n            Envelope::new(\n                KIND_NET_STREAM_OPENED,\n                serde_json::to_value(NetStreamOpened {\n                    stream_id,\n                    conn_id,\n                    op_id,\n                    applied_now,\n                })\n                .expect(\"NetStreamOpened serializes\"),\n            )\n        };\n        match req.op_id {\n            None => {\n                let stream_id = host.open_stream(req.conn_id).map_err(|e| e.to_string())?;\n                send_frame(send, &build(stream_id, req.conn_id, None, true));\n                Ok(())\n            }\n            Some(op) => {\n                let minter = req.minter.unwrap_or(Minter::Legacy);\n                let key = EffectKey {\n                    class: NET_EFFECT_SESSION,\n                    minter,\n                    op,\n                };\n                let outcome = self\n                    .journal\n                    .apply_once(key, EffectKind::NetSend, || {\n                        let stream_id = host.open_stream(req.conn_id)?;\n                        host.record_stream_op(minter, op, stream_id);\n                        Ok(())\n                    })\n                    .map_err(|e| format!(\"net-stream-open failed: {e}\"))?;\n                // Frozen wire-compat text, single-sourced from the shared marker so\n                // the A-4b retry matches one source of truth (ADR-0034 Addendum 2).\n                let stream_id = host\n                    .stream_op_id(minter, op)\n                    .ok_or_else(crate::effect::stream_open_no_longer_held_message)?;\n                let applied_now = outcome == Outcome::Applied;\n                send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));\n                Ok(())\n            }\n        }\n    }\n\n    /// Write bytes to a held stream's send side (D4b). With an `op_id` the\n    /// write is exactly-once through the broker-owned journal (a replayed\n    /// chunk after a brain crash is not written twice — Spike #3 invariant D\n    /// on the send path) and acked with `net-sent`; without one it is a direct\n    /// write (no dedup, no ack), mirroring the PTY `input` contract.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n        let bytes =\n            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        match req.op_id {\n            None => host\n                .send_stream(req.stream_id, &bytes, req.finish)\n                .map_err(|e| e.to_string()),\n            Some(op) => {\n                let minter = req.minter.unwrap_or(Minter::Legacy);\n                let key = EffectKey {\n                    class: NET_EFFECT_SESSION,\n                    minter,\n                    op,\n                };\n                let outcome = self\n                    .journal\n                    .apply_once(key, EffectKind::NetSend, || {\n                        host.send_stream(req.stream_id, &bytes, req.finish)\n                    })\n                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\n                let frame = Envelope::new(\n                    KIND_NET_SENT,\n                    serde_json::to_value(NetSent {\n                        stream_id: req.stream_id,\n                        op_id: op,\n                        applied_now: outcome == Outcome::Applied,\n                    })\n                    .expect(\"NetSent serializes\"),\n                );\n                send_frame(send, &frame);\n                Ok(())\n            }\n        }\n    }\n\n    /// List every stream the broker holds (D4b) — the query a re-attaching\n    /// brain uses to find streams that arrived while it was down.\n    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        let frame = Envelope::new(\n            KIND_NET_STREAMS_REPLY,\n            serde_json::to_value(NetStreamsReply {\n                streams: host.stream_infos(),\n            })\n            .expect(\"NetStreamsReply serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(())\n    }\n\n    /// Re-attach a brain to a stream's buffered reads from a cursor (D4b) —\n    /// the gapless-resume `subscribe` discipline on a QUIC stream.\n    // [impl->REQ-DAEMON-2]\n    fn dispatch_net_stream_subscribe(\n        &self,\n        env: Envelope,\n        send: &SharedSend,\n    ) -> Result<u64, String> {\n        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-subscribe payload: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        host.subscribe_stream(req.stream_id, Arc::clone(send), req.from_seq)\n            .map_err(|e| e.to_string())?;\n        Ok(req.stream_id)\n    }\n\n    /// Attach a brain to the conn-liveness event log from a cursor (D4c) — the\n    /// PresenceChannel broker seam: gapless resume over connection presence,\n    /// same discipline as a stream subscribe.\n    // [impl->REQ-EP-4]\n    fn dispatch_net_presence_subscribe(\n        &self,\n        env: Envelope,\n        send: &SharedSend,\n    ) -> Result<(), String> {\n        let req: NetPresenceSubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-presence-subscribe payload: {e}\"))?;\n…\n    }"}],"details":{"displayContent":{"text":"        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_INPUT => {\n                    if let Err(msg) = self.dispatch_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_KILL => {\n                    if let Err(msg) = self.dispatch_kill(env) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\n                KIND_ADAPTER_APPLY => {\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\n                KIND_NET_DIAL => {\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_DIAL_LOOPBACK => {\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-PUMP-PEER-ISOLATION]\n                KIND_NET_DIAL_SUBMIT => {\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_JOIN => {\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_MEET => {\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_CODE_SUBMIT => {\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPEN => {\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SEND => {\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAMS => {\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_SESSIONS => {\n                    // Clone the (id, endpoint, log) out from under the sessions\n                    // lock, then read each log's cursor under the log lock alone —\n                    // never both locks at once (the dispatch_subscribe discipline).\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n                        let sessions = recover(&self.sessions);\n                        sessions\n                            .iter()\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\n                            .collect()\n                    };\n                    // Snapshot each session's truth under its OWN log lock (cursor +\n                    // controller identity + the control/viewer stamp truth), doing NO\n                    // file I/O under the lock (the lock-across-effect discipline, KH\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\n                    // the lock.\n                    struct SessSnap {\n                        id: u64,\n                        endpoint: String,\n                        resume_seq: u64,\n                        controller_by: Option<String>,\n                        has_controller: bool,\n                        viewers: u32,\n                    }\n                    let snaps: Vec<SessSnap> = rows\n                        .into_iter()\n                        .map(|(id, endpoint, log)| {\n                            let mut log = recover_log(&log);\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\n                            // truth HONEST before reading it — reap a dead-writer\n                            // (severed) controller so both the reply's controller_by\n                            // AND the off-lock converge_perch_stamps below see the\n                            // cleared state and the stale info.json stamp clears.\n                            let _ = log.reap_dead_controller();\n                            SessSnap {\n                                id,\n                                endpoint,\n                                resume_seq: log.delivered_through(),\n                                controller_by: log.controller_by().map(str::to_string),\n                                has_controller: log.has_controller(),\n                                viewers: log.live_viewer_count(),\n                            }\n                        })\n                        .collect();\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        let sessions = recover(&self.sessions);\n        for id in &my_subs {\n            if let Some(h) = sessions.get(id) {\n                recover_log(&h.log).detach_if(&send);\n            }\n        }\n        drop(sessions);\n        // Same for net streams: the streams + their rings persist (D4b).\n        if let Some(host) = self.net.get() {\n            for id in &my_stream_subs {\n                host.detach_stream_sub(*id, &send);\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n…\n        }\n…\n    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\n…\n            Err(fail) => {\n                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony\n                // against the same held address); every other verdict is terminal.\n                if !fail_keeps_hold(&fail) {\n                    recover(&self.pair_holds).remove(&req.session_id);\n                }\n                pair_fail_frame(&fail)\n            }\n        };\n        send_frame(send, &frame);\n        Ok(())\n    }\n\n    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once\n    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted\n    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped\n    /// replay is answered with the stream the first apply opened.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        let build = |stream_id: u64, conn_id: u64, op_id: Option<u64>, applied_now: bool| {\n            Envelope::new(\n                KIND_NET_STREAM_OPENED,\n                serde_json::to_value(NetStreamOpened {\n                    stream_id,\n                    conn_id,\n                    op_id,\n                    applied_now,\n                })\n                .expect(\"NetStreamOpened serializes\"),\n            )\n        };\n        match req.op_id {\n            None => {\n                let stream_id = host.open_stream(req.conn_id).map_err(|e| e.to_string())?;\n                send_frame(send, &build(stream_id, req.conn_id, None, true));\n                Ok(())\n            }\n            Some(op) => {\n                let minter = req.minter.unwrap_or(Minter::Legacy);\n                let key = EffectKey {\n                    class: NET_EFFECT_SESSION,\n                    minter,\n                    op,\n                };\n                let outcome = self\n                    .journal\n                    .apply_once(key, EffectKind::NetSend, || {\n                        let stream_id = host.open_stream(req.conn_id)?;\n                        host.record_stream_op(minter, op, stream_id);\n                        Ok(())\n                    })\n                    .map_err(|e| format!(\"net-stream-open failed: {e}\"))?;\n                // Frozen wire-compat text, single-sourced from the shared marker so\n                // the A-4b retry matches one source of truth (ADR-0034 Addendum 2).\n                let stream_id = host\n                    .stream_op_id(minter, op)\n                    .ok_or_else(crate::effect::stream_open_no_longer_held_message)?;\n                let applied_now = outcome == Outcome::Applied;\n                send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));\n                Ok(())\n            }\n        }\n    }\n\n    /// Write bytes to a held stream's send side (D4b). With an `op_id` the\n    /// write is exactly-once through the broker-owned journal (a replayed\n    /// chunk after a brain crash is not written twice — Spike #3 invariant D\n    /// on the send path) and acked with `net-sent`; without one it is a direct\n    /// write (no dedup, no ack), mirroring the PTY `input` contract.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n        let bytes =\n            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        match req.op_id {\n            None => host\n                .send_stream(req.stream_id, &bytes, req.finish)\n                .map_err(|e| e.to_string()),\n            Some(op) => {\n                let minter = req.minter.unwrap_or(Minter::Legacy);\n                let key = EffectKey {\n                    class: NET_EFFECT_SESSION,\n                    minter,\n                    op,\n                };\n                let outcome = self\n                    .journal\n                    .apply_once(key, EffectKind::NetSend, || {\n                        host.send_stream(req.stream_id, &bytes, req.finish)\n                    })\n                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\n                let frame = Envelope::new(\n                    KIND_NET_SENT,\n                    serde_json::to_value(NetSent {\n                        stream_id: req.stream_id,\n                        op_id: op,\n                        applied_now: outcome == Outcome::Applied,\n                    })\n                    .expect(\"NetSent serializes\"),\n                );\n                send_frame(send, &frame);\n                Ok(())\n            }\n        }\n    }\n\n    /// List every stream the broker holds (D4b) — the query a re-attaching\n    /// brain uses to find streams that arrived while it was down.\n    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        let frame = Envelope::new(\n            KIND_NET_STREAMS_REPLY,\n            serde_json::to_value(NetStreamsReply {\n                streams: host.stream_infos(),\n            })\n            .expect(\"NetStreamsReply serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(())\n    }\n\n    /// Re-attach a brain to a stream's buffered reads from a cursor (D4b) —\n    /// the gapless-resume `subscribe` discipline on a QUIC stream.\n    // [impl->REQ-DAEMON-2]\n    fn dispatch_net_stream_subscribe(\n        &self,\n        env: Envelope,\n        send: &SharedSend,\n    ) -> Result<u64, String> {\n        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-subscribe payload: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        host.subscribe_stream(req.stream_id, Arc::clone(send), req.from_seq)\n            .map_err(|e| e.to_string())?;\n        Ok(req.stream_id)\n    }\n\n    /// Attach a brain to the conn-liveness event log from a cursor (D4c) — the\n    /// PresenceChannel broker seam: gapless resume over connection presence,\n    /// same discipline as a stream subscribe.\n    // [impl->REQ-EP-4]\n    fn dispatch_net_presence_subscribe(\n        &self,\n        env: Envelope,\n        send: &SharedSend,\n    ) -> Result<(), String> {\n        let req: NetPresenceSubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-presence-subscribe payload: {e}\"))?;\n…\n    }","startLine":3010,"lineNumbers":[3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,null,3214,null,4124,null,4165,null,4179,4180,4181,4182,4183,4184,4185,4186,4187,4188,4189,4190,4191,4192,4193,4194,4195,4196,4197,4198,4199,4200,4201,4202,4203,4204,4205,4206,4207,4208,4209,4210,4211,4212,4213,4214,4215,4216,4217,4218,4219,4220,4221,4222,4223,4224,4225,4226,4227,4228,4229,4230,4231,4232,4233,4234,4235,4236,4237,4238,4239,4240,4241,4242,4243,4244,4245,4246,4247,4248,4249,4250,4251,4252,4253,4254,4255,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4294,4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307,4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326,4327,4328,4329,4330,4331,null,4335]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784195883400}}
{"type":"message","id":"318e67e6","parentId":"3c3fbb55","timestamp":"2026-07-16T09:58:03.421Z","message":{"role":"toolResult","toolCallId":"call_rM3hgUyrT0fQ0lczENr7jSPU|fc_0698befac979932d016a58ab2abdc0819aad8603c19115b2c2","toolName":"read","content":[{"type":"text","text":"///\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n/// construction/teardown, the parting copy. This fn only needs an established\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\n/// viewport was attached to (a reconnect can re-resolve it).\n#[allow(clippy::too_many_arguments)]\nfn attach_viewport(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    view: bool,\n    mut est: Established,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n    remote_node: &mut Option<String>,\n    out: &mut impl std::io::Write,\n) -> Result<PumpEnd, String> {\n    loop {\n        // Controller: send the INITIAL terminal size so the PTY matches from the\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\n        // size. When the status row is active the harness PTY gets rows-1 (we own\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\n        // viewport's PTY must match our CURRENT terminal, not the original one.\n        if !view {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                let pty_rows = if status.is_some() {\n                    rows.saturating_sub(1).max(1)\n                } else {\n                    rows\n                };\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\n            }\n        }\n\n        let mut decoder = AttachDecoder::new();\n        let end = pump(\n            &mut est.brain,\n            est.stream_id,\n            &est.ops,\n            rx,\n            &mut decoder,\n            out,\n            !view, // controller drives resize\n            mouse_mode,\n            status.as_deref_mut(),\n        );\n\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\n        let severed_kind = match &end {\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n                _ => \"the connection was severed\",\n            },\n            _ => return end,\n        };\n\n        // Reconnect: full-screen centered banner (operator language — the internal\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\n        // establish_attach inside the bounded window. A detach keypress mid-window\n        // aborts the retry loop honestly. The banner repaints each iteration with a\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\n        let retry_started = Instant::now();\n        let reestablished = loop {\n            let elapsed = retry_started.elapsed();\n            if reconnect_expired(elapsed) {\n                break None;\n            }\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n            let remaining = reconnect_remaining_secs(elapsed);\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n            let _ = out.flush();\n            // Operator gives up mid-reconnect: honor the detach keys.\n            if let Some(rx) = rx {\n                let mut detached = false;\n                while let Ok(msg) = rx.try_recv() {\n                    if matches!(msg, StdinMsg::Detach) {\n                        detached = true;\n                    }\n                }\n                if detached {\n                    break Some(Err(())); // detach sentinel\n                }\n            }\n            match establish_attach(endpoint_id, intent) {\n                Ok(next) => break Some(Ok(next)),\n                // Every fail shape retries until the window expires — the node/daemon\n                // may still be coming back. DaemonDown included: rc waits for the\n                // operator to restart it, it does NOT auto-start one.\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\n            }\n        };\n        match reestablished {\n            Some(Ok(next)) => {\n                *remote_node = next.remote_node.clone();\n                est = next;\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\n                // repainting the screen the banner cleared.\n            }\n            Some(Err(())) => return Ok(PumpEnd::Detached),\n            None => {\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\n                // lost — daemon down\" copy; anything else keeps the generic\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\n                let daemon_down =\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\n                return Ok(PumpEnd::ReconnectGaveUp {\n                    detail: severed_kind.to_string(),\n                    daemon_down,\n                });\n            }\n        }\n    }\n}\n\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\n/// node's broker restart both land well inside it.\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n/// The pause between reconnect attempts (each attempt already carries its own\n/// bounded setup deadlines — this just keeps the loop from hammering).\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_expired(elapsed: Duration) -> bool {\n    elapsed >= RECONNECT_WINDOW\n}\n\n/// Seconds still left in the reconnect window, for the banner countdown\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\n/// the window is fully spent, where the caller has already broken to give-up).\n// [impl->REQ-RC-RECONNECT-TRUTH]\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n    (left.as_millis() as u64).div_ceil(1000)\n}\n\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum EndClass {\n    Final,\n    Reconnect,\n}\n\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n/// Everything else — session exit, operator detach, displacement, the stall\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n/// session the operator (or the session itself) deliberately ended.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_class(end: &PumpEnd) -> EndClass {\n    match end {\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n        PumpEnd::Exited(_)\n        | PumpEnd::Detached\n        | PumpEnd::Displaced(_)\n        | PumpEnd::Stalled\n        | PumpEnd::NoLiveSession\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\n    }\n}\n\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n// [impl->REQ-RC-RECONNECT]\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\n    if rendered_any {\n        PumpEnd::Severed\n    } else {\n        PumpEnd::NoLiveSession\n    }\n}\n\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\n    // never mistakes a bounded retry for the historical freeze-forever.\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\n    let row = (rows / 2).max(1);\n    // Center on the CHARACTER count (the terminal cell count for this ASCII+…\n    // line), clamped so an over-wide text still starts on-screen.\n    let width = text.chars().count() as u16;\n…\n}\n…\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n…\n}"}],"details":{"displayContent":{"text":"///\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n/// construction/teardown, the parting copy. This fn only needs an established\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\n/// viewport was attached to (a reconnect can re-resolve it).\n#[allow(clippy::too_many_arguments)]\nfn attach_viewport(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    view: bool,\n    mut est: Established,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n    remote_node: &mut Option<String>,\n    out: &mut impl std::io::Write,\n) -> Result<PumpEnd, String> {\n    loop {\n        // Controller: send the INITIAL terminal size so the PTY matches from the\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\n        // size. When the status row is active the harness PTY gets rows-1 (we own\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\n        // viewport's PTY must match our CURRENT terminal, not the original one.\n        if !view {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                let pty_rows = if status.is_some() {\n                    rows.saturating_sub(1).max(1)\n                } else {\n                    rows\n                };\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\n            }\n        }\n\n        let mut decoder = AttachDecoder::new();\n        let end = pump(\n            &mut est.brain,\n            est.stream_id,\n            &est.ops,\n            rx,\n            &mut decoder,\n            out,\n            !view, // controller drives resize\n            mouse_mode,\n            status.as_deref_mut(),\n        );\n\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\n        let severed_kind = match &end {\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n                _ => \"the connection was severed\",\n            },\n            _ => return end,\n        };\n\n        // Reconnect: full-screen centered banner (operator language — the internal\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\n        // establish_attach inside the bounded window. A detach keypress mid-window\n        // aborts the retry loop honestly. The banner repaints each iteration with a\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\n        let retry_started = Instant::now();\n        let reestablished = loop {\n            let elapsed = retry_started.elapsed();\n            if reconnect_expired(elapsed) {\n                break None;\n            }\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n            let remaining = reconnect_remaining_secs(elapsed);\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n            let _ = out.flush();\n            // Operator gives up mid-reconnect: honor the detach keys.\n            if let Some(rx) = rx {\n                let mut detached = false;\n                while let Ok(msg) = rx.try_recv() {\n                    if matches!(msg, StdinMsg::Detach) {\n                        detached = true;\n                    }\n                }\n                if detached {\n                    break Some(Err(())); // detach sentinel\n                }\n            }\n            match establish_attach(endpoint_id, intent) {\n                Ok(next) => break Some(Ok(next)),\n                // Every fail shape retries until the window expires — the node/daemon\n                // may still be coming back. DaemonDown included: rc waits for the\n                // operator to restart it, it does NOT auto-start one.\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\n            }\n        };\n        match reestablished {\n            Some(Ok(next)) => {\n                *remote_node = next.remote_node.clone();\n                est = next;\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\n                // repainting the screen the banner cleared.\n            }\n            Some(Err(())) => return Ok(PumpEnd::Detached),\n            None => {\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\n                // lost — daemon down\" copy; anything else keeps the generic\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\n                let daemon_down =\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\n                return Ok(PumpEnd::ReconnectGaveUp {\n                    detail: severed_kind.to_string(),\n                    daemon_down,\n                });\n            }\n        }\n    }\n}\n\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\n/// node's broker restart both land well inside it.\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n/// The pause between reconnect attempts (each attempt already carries its own\n/// bounded setup deadlines — this just keeps the loop from hammering).\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_expired(elapsed: Duration) -> bool {\n    elapsed >= RECONNECT_WINDOW\n}\n\n/// Seconds still left in the reconnect window, for the banner countdown\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\n/// the window is fully spent, where the caller has already broken to give-up).\n// [impl->REQ-RC-RECONNECT-TRUTH]\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n    (left.as_millis() as u64).div_ceil(1000)\n}\n\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum EndClass {\n    Final,\n    Reconnect,\n}\n\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n/// Everything else — session exit, operator detach, displacement, the stall\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n/// session the operator (or the session itself) deliberately ended.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_class(end: &PumpEnd) -> EndClass {\n    match end {\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n        PumpEnd::Exited(_)\n        | PumpEnd::Detached\n        | PumpEnd::Displaced(_)\n        | PumpEnd::Stalled\n        | PumpEnd::NoLiveSession\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\n    }\n}\n\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n// [impl->REQ-RC-RECONNECT]\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\n    if rendered_any {\n        PumpEnd::Severed\n    } else {\n        PumpEnd::NoLiveSession\n    }\n}\n\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\n    // never mistakes a bounded retry for the historical freeze-forever.\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\n    let row = (rows / 2).max(1);\n    // Center on the CHARACTER count (the terminal cell count for this ASCII+…\n    // line), clamped so an over-wide text still starts on-screen.\n    let width = text.chars().count() as u16;\n…\n}\n…\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n…\n}","startLine":1510,"lineNumbers":[1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,null,1717,null,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,null,3213]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784195883421}}
{"type":"message","id":"7672372e","parentId":"318e67e6","timestamp":"2026-07-16T09:58:03.423Z","message":{"role":"toolResult","toolCallId":"call_vF1WTNJii7Y9DM7unwqTfy9n|fc_0698befac979932d016a58ab2abe48819a8ac4ab86832b902f","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/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...\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\n## spt-daemon/tests/\n### attach.rs\n 141|/// then detach and confirm the serve loop ended `Detached` (session outlives).\n*142|/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143|/// caller can assert session survival and clean up.\n 144|#[allow(clippy::too_many_arguments)]\n 145|fn attach_drive_detach(\n...\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"}],"details":{"scopePath":"crates/spt/tests, crates/spt-daemon/tests, crates/spt/src/rc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":7,"fileCount":2,"files":["crates/spt/tests/daemon_refresh_e2e.rs","crates/spt-daemon/tests/attach.rs"],"fileMatches":[{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":5},{"path":"crates/spt-daemon/tests/attach.rs","count":2}],"truncated":false,"displayContent":"# crates/\n\n## spt/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    │...\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\n## spt-daemon/tests/\n### attach.rs\n 141│/// then detach and confirm the serve loop ended `Detached` (session outlives).\n*142│/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143│/// caller can assert session survival and clean up.\n 144│#[allow(clippy::too_many_arguments)]\n 145│fn attach_drive_detach(\n    │...\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"},"isError":false,"timestamp":1784195883423}}
{"type":"message","id":"064eaa9c","parentId":"7672372e","timestamp":"2026-07-16T09:58:03.428Z","message":{"role":"toolResult","toolCallId":"call_lwSmY3D0I7Utx4lV2nMbeRZs|fc_0698befac979932d016a58ab2abe14819a8504d42e4ad92d26","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n\n### tests/\n#### attach.rs\n 13|//!   on the stream table's handshake-proven `remote_id_hex`;\n*14|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n 15|//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n 16|//!   log, and the effect journal. The successor brain re-serves the SAME\n*17|//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n 18|//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n 19|//!   output dedups at B's render cursor (rendered exactly once) — including\n 20|//!   input B sent INTO the dead window.\n...\n 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n 29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n...\n 45|/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n*46|/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n 47|/// `driven_by` marker, and that resolution must never touch a real home.\n 48|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n 49|\n...\n 133|/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in\n*134|/// `expected_origin`, never in the `serve_attach`+render pump below. There is no\n 135|/// second code path that could drift.\n 136|///\n 137|/// Given an acquired `conn_id` on `operator` and the `target` broker connection\n...\n 171|        let outcome =\n*172|            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0, None)\n 173|                .expect(\"serve\");\n 174|        (outcome, target)\n 175|    });\n...\n 222|                        if seq < *next_seq {\n*223|                            continue; // replay duplicate — dedup by cursor\n 224|                        }\n 225|                        // #6 (ADR-0031): the initial batch is now a single synthesized\n 226|                        // repaint at pseudo-seq `log.next_seq - 1`, so the FIRST frame a\n...\n 266|// holds via the fallback transport (a broker-local loopback conn feeding the same\n*267|// `serve_attach` pump), NOT a self-dial — exactly the doyle-approved fallback.\n 268|// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n 269|#[test]\n 270|fn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n...\n 343|        let outcome =\n*344|            serve_attach(&mut target, &serve_broker, stream_a, \"cc-ep\", &origin, 0, None).expect(\"serve\");\n 345|        (outcome, target)\n 346|    });\n 347|\n...\n 420|// self-dial, which iroh refuses) and drives the PTY through the SAME\n*421|// `serve_attach`+render pump a cross-node operator uses. The proof that \"both\n 422|// transports → one pump\" is BY CONSTRUCTION: this test and the cross-node\n 423|// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n 424|// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n...\n 467|\n*468|// [unit->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT] the replay-vs-forward deadlock pin:\n 469|// when the session's OutputLog ring is ALREADY NON-EMPTY at attach (an\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 475|// could not read the serve→broker forwards: mutual backpressure DEADLOCK, operator\n*476|// got ZERO bytes. With `serve_attach` forwarding on a SEPARATE conn the full\n*477|// populated ring replays and is delivered in order. The prior loopback E2Es only\n*478|// ever attached to an idle/just-spawned child (empty ring → 0-frame replay), so\n 479|// none exercised this — the regression the suite lacked.\n 480|#[test]\n 481|fn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\n...\n 488|    // so it never exits before we attach. `TICK39` (the last line) proves the whole\n*489|    // populated ring replayed + forwarded in order — not just the first chunk.\n 490|    #[cfg(unix)]\n 491|    let (program, args) = (\n 492|        \"sh\".to_string(),\n...\n 507|    // The SPAWNING conn drains the burst live into the ring, then we DROP it — so a\n*508|    // fresh attach must replay the now non-empty ring (the endpoint-run shape).\n 509|    let mut spawner = connect_retry(&name);\n 510|    let sid = spawner\n 511|        .spawn_session(SpawnReq {\n...\n 527|        .expect(\"the burst lands in the ring\");\n*528|    drop(spawner); // spawn conn gone — a fresh attach must replay the populated ring\n 529|\n 530|    // A fresh operator attaches over loopback (the real `spt rc` transport).\n 531|    let mut operator = connect_retry(&name);\n#### dispatch.rs\n 771|    // share ONE canonical store, and a store-side produce would make B's\n*772|    // apply an Unchanged replay — a real remote producer's row is a fresh\n 773|    // insert at B, which is what the redirect surfaces on.)\n 774|    let store = NotifStore::open().expect(\"store\");\n 775|    let policy = spt_daemon::NotifSurfacePolicy::load();\n\n### src/\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 170|/// worker (policies/stores re-load from the canonical paths per request —\n*171|/// the access-gate stance, config edits take effect without a restart).\n 172|#[derive(Debug, Clone)]\n 173|pub struct DispatchPaths {\n 174|    /// The perch tree (WAN message funnel target).\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 548|/// MRA also calls remote stays unmarked and rides on (someone else's\n*549|/// feed-apply surfaces it); replays merge `Unchanged` and never re-fire.\n 550|// [impl->REQ-NOTIF-1]\n 551|// [impl->REQ-PRES-1]\n 552|fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {\n\n## spt/tests/\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 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 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 229|    //    HARD (the D4 hard-kill posture — the outgoing side is gone before the\n*230|    //    supervised child resumes). ──\n 231|    let (sid, child_pid) = {\n 232|        let mut driver = connect_retry(&broker_socket_name());\n 233|\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 291|\n*292|    // ── Generation 0: the real child came up from fixture A, resumed the hosted\n*293|    //    session (the first N≥1 cursor-of-record resume), and stamped exe_hash A. ──\n 294|    let (pid0, _gen0, hash0) = wait_ready(&ready_path, None, Duration::from_secs(45))\n 295|        .unwrap_or_else(|| teardown_panic(&stop, \"gen-0 brain never signalled ready\"));\n 296|    assert_eq!(\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*305|        \"the PTY child's pid is unchanged through the resume\"\n 306|    );\n 307|    assert_eq!(broker.net_conn_count(), 1, \"the QUIC conn survived the driver drop\");\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-daemon/tests/attach.rs, crates/spt-daemon/tests/dispatch.rs, crates/spt-daemon/src/dispatch.rs, crates/spt/tests/brain_survive.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":42,"fileCount":4,"files":["crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/src/dispatch.rs","crates/spt/tests/brain_survive.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/dispatch.rs","count":1},{"path":"crates/spt-daemon/src/dispatch.rs","count":5},{"path":"crates/spt/tests/brain_survive.rs","count":16}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/\n\n### tests/\n#### attach.rs\n  13│//!   on the stream table's handshake-proven `remote_id_hex`;\n *14│//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n  15│//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n  16│//!   log, and the effect journal. The successor brain re-serves the SAME\n *17│//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n  18│//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n  19│//!   output dedups at B's render cursor (rendered exactly once) — including\n  20│//!   input B sent INTO the dead window.\n    │...\n  27│use spt_daemon::attach::{\n *28│    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n  29│    AttachServeOutcome, ControllerIrrecoverablyBehind,\n  30│};\n  31│// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n    │...\n  45│/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n *46│/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n  47│/// `driven_by` marker, and that resolution must never touch a real home.\n  48│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n  49│\n    │...\n 133│/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in\n*134│/// `expected_origin`, never in the `serve_attach`+render pump below. There is no\n 135│/// second code path that could drift.\n 136│///\n 137│/// Given an acquired `conn_id` on `operator` and the `target` broker connection\n    │...\n 171│        let outcome =\n*172│            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0, None)\n 173│                .expect(\"serve\");\n 174│        (outcome, target)\n 175│    });\n    │...\n 222│                        if seq < *next_seq {\n*223│                            continue; // replay duplicate — dedup by cursor\n 224│                        }\n 225│                        // #6 (ADR-0031): the initial batch is now a single synthesized\n 226│                        // repaint at pseudo-seq `log.next_seq - 1`, so the FIRST frame a\n    │...\n 266│// holds via the fallback transport (a broker-local loopback conn feeding the same\n*267│// `serve_attach` pump), NOT a self-dial — exactly the doyle-approved fallback.\n 268│// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n 269│#[test]\n 270│fn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n    │...\n 343│        let outcome =\n*344│            serve_attach(&mut target, &serve_broker, stream_a, \"cc-ep\", &origin, 0, None).expect(\"serve\");\n 345│        (outcome, target)\n 346│    });\n 347│\n    │...\n 420│// self-dial, which iroh refuses) and drives the PTY through the SAME\n*421│// `serve_attach`+render pump a cross-node operator uses. The proof that \"both\n 422│// transports → one pump\" is BY CONSTRUCTION: this test and the cross-node\n 423│// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n 424│// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n    │...\n 467│\n*468│// [unit->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT] the replay-vs-forward deadlock pin:\n 469│// when the session's OutputLog ring is ALREADY NON-EMPTY at attach (an\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 475│// could not read the serve→broker forwards: mutual backpressure DEADLOCK, operator\n*476│// got ZERO bytes. With `serve_attach` forwarding on a SEPARATE conn the full\n*477│// populated ring replays and is delivered in order. The prior loopback E2Es only\n*478│// ever attached to an idle/just-spawned child (empty ring → 0-frame replay), so\n 479│// none exercised this — the regression the suite lacked.\n 480│#[test]\n 481│fn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\n    │...\n 488│    // so it never exits before we attach. `TICK39` (the last line) proves the whole\n*489│    // populated ring replayed + forwarded in order — not just the first chunk.\n 490│    #[cfg(unix)]\n 491│    let (program, args) = (\n 492│        \"sh\".to_string(),\n    │...\n 507│    // The SPAWNING conn drains the burst live into the ring, then we DROP it — so a\n*508│    // fresh attach must replay the now non-empty ring (the endpoint-run shape).\n 509│    let mut spawner = connect_retry(&name);\n 510│    let sid = spawner\n 511│        .spawn_session(SpawnReq {\n    │...\n 527│        .expect(\"the burst lands in the ring\");\n*528│    drop(spawner); // spawn conn gone — a fresh attach must replay the populated ring\n 529│\n 530│    // A fresh operator attaches over loopback (the real `spt rc` transport).\n 531│    let mut operator = connect_retry(&name);\n#### dispatch.rs\n 771│    // share ONE canonical store, and a store-side produce would make B's\n*772│    // apply an Unchanged replay — a real remote producer's row is a fresh\n 773│    // insert at B, which is what the redirect surfaces on.)\n 774│    let store = NotifStore::open().expect(\"store\");\n 775│    let policy = spt_daemon::NotifSurfacePolicy::load();\n\n### src/\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 170│/// worker (policies/stores re-load from the canonical paths per request —\n*171│/// the access-gate stance, config edits take effect without a restart).\n 172│#[derive(Debug, Clone)]\n 173│pub struct DispatchPaths {\n 174│    /// The perch tree (WAN message funnel target).\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 548│/// MRA also calls remote stays unmarked and rides on (someone else's\n*549│/// feed-apply surfaces it); replays merge `Unchanged` and never re-fire.\n 550│// [impl->REQ-NOTIF-1]\n 551│// [impl->REQ-PRES-1]\n 552│fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {\n\n## spt/tests/\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  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  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 229│    //    HARD (the D4 hard-kill posture — the outgoing side is gone before the\n*230│    //    supervised child resumes). ──\n 231│    let (sid, child_pid) = {\n 232│        let mut driver = connect_retry(&broker_socket_name());\n 233│\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 291│\n*292│    // ── Generation 0: the real child came up from fixture A, resumed the hosted\n*293│    //    session (the first N≥1 cursor-of-record resume), and stamped exe_hash A. ──\n 294│    let (pid0, _gen0, hash0) = wait_ready(&ready_path, None, Duration::from_secs(45))\n 295│        .unwrap_or_else(|| teardown_panic(&stop, \"gen-0 brain never signalled ready\"));\n 296│    assert_eq!(\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*305│        \"the PTY child's pid is unchanged through the resume\"\n 306│    );\n 307│    assert_eq!(broker.net_conn_count(), 1, \"the QUIC conn survived the driver drop\");\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":1784195883428}}
{"type":"message","id":"6cdbf95b","parentId":"064eaa9c","timestamp":"2026-07-16T09:58:03.446Z","message":{"role":"toolResult","toolCallId":"call_YJYlmTLUoeI9qQ26cwla3tYy|fc_0698befac979932d016a58ab2abda4819a81d090f4cdd8a7f7","toolName":"read","content":[{"type":"text","text":"/// 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\n[Showing lines 314-378 of 2116. Use :379 to continue]"}],"details":{"truncation":{"content":"/// 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","truncated":true,"truncatedBy":"lines","totalLines":1803,"totalBytes":3506,"outputLines":65,"outputBytes":3506,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// 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","startLine":314,"lineNumbers":[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]},"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":3506,"outputLines":65,"outputBytes":3506,"shownRange":{"start":314,"end":378},"nextOffset":379}}},"isError":false,"timestamp":1784195883446}}
{"type":"message","id":"e0858738","parentId":"6cdbf95b","timestamp":"2026-07-16T09:58:13.113Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing dispatcher spawn loop for reclaim behavior**"},{"type":"toolCall","id":"call_RR65JwrV2GswhZRNKAWpdBY7|fc_0698befac979932d016a58ab349be4819a9b45e2b4f2399c32","name":"read","arguments":{"i":"Reading dispatcher stream reclaim loop","path":"crates/spt-daemon/src/dispatch.rs","selector":"1-180,180-315"}},{"type":"toolCall","id":"call_TK9xU219ICGCpmWgzM7ZQkQN|fc_0698befac979932d016a58ab349bfc819a8f1acecdb74dab68","name":"read","arguments":{"i":"Reading restart survival attach test","path":"crates/spt-daemon/tests/attach.rs","selector":"760-915"}},{"type":"toolCall","id":"call_1YCdPxqZXa44uN8izGI00Icp|fc_0698befac979932d016a58ab349c08819a811ce865b1a267fc","name":"grep","arguments":{"i":"Finding dispatcher claim state","pattern":"claim|claimed|HashSet|active|in_flight|initiated_locally|stream_infos|spawn\\(move","path":"crates/spt-daemon/src/dispatch.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_lROWUje4oOcQ680ru0Nfz2oz|fc_0698befac979932d016a58ab349c18819a9d27a68d15884a53","name":"grep","arguments":{"i":"Finding stream subscriber detach methods","pattern":"fn subscribe_stream|fn detach_stream_sub|struct StreamLog|fn attach|subscriber","path":"crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_jCbFCEXp0iWHzlYISSDhfq6I|fc_0698befac979932d016a58ab349c20819aba2fe9c6cf739e39","name":"read","arguments":{"i":"Reading live endpoint refresh behavior","path":"crates/spt-daemon/src/livehost.rs","selector":"960-1020"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":23549,"output":302,"cacheRead":129536,"cacheWrite":0,"totalTokens":153387,"reasoningTokens":23,"cost":{"input":0.11774500000000002,"output":0.00906,"cacheRead":0.06476799999999999,"cacheWrite":0,"total":0.19157300000000002}},"stopReason":"toolUse","timestamp":1784195883495,"responseId":"resp_0698befac979932d016a58ab2bed7c819ab4a11e51e6bd6ebb","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ab2f1cec819a8f1b396040fdee44","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKs0Y_ZO_q9rYzuxoDgfpUMqoJlXG6gcel1tc-73XFIE7h-qfwbqEe0MKyLrElBr2E752alby5AgyAasUcAIqVuAOj9269Ie5gHrMy9w_D_TAFlRoRolfZ3eX09sMl7nKnx15SV_-DnKOcroNOqNq_H0AgTqC9wmg82JKyIqwuVoLpe_LMVz4Ytejtc9KQEzx7KM4weJXS-Spy7soVbyGpVSkMNeao_UYaB2slw5EuC9FGn6126pip32_ihQ70mttx3DV-__MZq4S1bnJ9G8zqT2kIMbjUsuo1VZMI5i4OVbyxTcWdSNFf2y5nOYnqIw4gWCnHf_yJJRzRQmAcgFAIsmZL0icYEhtSFY4uupHIABq0AyaWjSKALR3XPbW-l28Ri6w0ITEFTGuSUFY_W7Ke4pB39hPBu79eh816985r4ux3d2Izsrx6JfAaquXrACvmIXubNDh5e5DeOXm9-B7hpHZ3MqsGfm8ZY9ddTRx-LzGpLoy9k5SyxSYKP_NBVRL3l1mqJNXeEtYAae2vxjqDV954jGJbIQAWOtqUCvoy1PCRSZLMTqXNdIzv2HThaB_y4r44AUpOUZEMAXZ1g61nf07FyUxne2ol7uonfDoFJ_qr9ewatJbtUcKaVxGnp2r3qo1YAHPmxslf2RA4SNy30KxR4nFUtSWdWsH0ejbylT2SGbybkyhKOWgBApQ-6mo8vUe5C0lH7SDpVMii6gSBNq-nKdtf8h--6-hE0LS6e-gwQ20CLq68dQN3MNFfXCvj-zPFlZFJoX6Jvzx6G95OBxYyoMIkfXtZSYGMXu1BA3QInA8BNobgntAtku52Ob3xmemAR-6vZytKUqSLf-L-ovm7WjrGGDTpvZcjqGEFT2EA5tscaRjAFwuvNDfr0kfa4pJB5tKGSrB5hM0kQ6q_E9KDm7COIA8ZsBu4wh9X1LmvXjAM11dNbIZDF_ABRvPwbnO0dIYgQzSjRXSchAS7ZHU43sH8hK3J_VqCRXQuBNwOoap99fprSDataUz8u8VrHzdMwler0rkp-aSwa-yehvyhU6oZzbRaJYBsQdcOD09Sw=","summary":[{"type":"summary_text","text":"**Analyzing dispatcher spawn loop for reclaim behavior**"}]},{"id":"fc_0698befac979932d016a58ab349be4819a9b45e2b4f2399c32","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher stream reclaim loop\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"1-180,180-315\"}","call_id":"call_RR65JwrV2GswhZRNKAWpdBY7","name":"read"},{"id":"fc_0698befac979932d016a58ab349bfc819a8f1acecdb74dab68","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading restart survival attach test\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"760-915\"}","call_id":"call_TK9xU219ICGCpmWgzM7ZQkQN","name":"read"},{"id":"fc_0698befac979932d016a58ab349c08819a811ce865b1a267fc","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatcher claim state\",\"pattern\":\"claim|claimed|HashSet|active|in_flight|initiated_locally|stream_infos|spawn\\\\(move\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_1YCdPxqZXa44uN8izGI00Icp","name":"grep"},{"id":"fc_0698befac979932d016a58ab349c18819a9d27a68d15884a53","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding stream subscriber detach methods\",\"pattern\":\"fn subscribe_stream|fn detach_stream_sub|struct StreamLog|fn attach|subscriber\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_lROWUje4oOcQ680ru0Nfz2oz","name":"grep"},{"id":"fc_0698befac979932d016a58ab349c20819aba2fe9c6cf739e39","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading live endpoint refresh behavior\",\"path\":\"crates/spt-daemon/src/livehost.rs\",\"selector\":\"960-1020\"}","call_id":"call_jCbFCEXp0iWHzlYISSDhfq6I","name":"read"}]},"duration":9602.579400000162,"ttft":3597.3031999999657,"contextSnapshot":{"promptTokens":153085,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RR65JwrV2GswhZRNKAWpdBY7|fc_0698befac979932d016a58ab349be4819a9b45e2b4f2399c32","toolName":"read","startedAt":"2026-07-16T09:58:13.114Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher stream reclaim loop"},"id":"72987314","parentId":"e0858738","timestamp":"2026-07-16T09:58:13.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TK9xU219ICGCpmWgzM7ZQkQN|fc_0698befac979932d016a58ab349bfc819a8f1acecdb74dab68","toolName":"read","startedAt":"2026-07-16T09:58:13.114Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading restart survival attach test"},"id":"32581ce4","parentId":"72987314","timestamp":"2026-07-16T09:58:13.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1YCdPxqZXa44uN8izGI00Icp|fc_0698befac979932d016a58ab349c08819a811ce865b1a267fc","toolName":"grep","startedAt":"2026-07-16T09:58:13.114Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Finding dispatcher claim state"},"id":"cc8a8371","parentId":"32581ce4","timestamp":"2026-07-16T09:58:13.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lROWUje4oOcQ680ru0Nfz2oz|fc_0698befac979932d016a58ab349c18819a9d27a68d15884a53","toolName":"grep","startedAt":"2026-07-16T09:58:13.114Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Finding stream subscriber detach methods"},"id":"57cfc449","parentId":"cc8a8371","timestamp":"2026-07-16T09:58:13.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jCbFCEXp0iWHzlYISSDhfq6I|fc_0698befac979932d016a58ab349c20819aba2fe9c6cf739e39","toolName":"read","startedAt":"2026-07-16T09:58:13.114Z","args":{"path":"crates/spt-daemon/src/livehost.rs"},"intent":"Reading live endpoint refresh behavior"},"id":"44f3d1d8","parentId":"57cfc449","timestamp":"2026-07-16T09:58:13.114Z"}
{"type":"message","id":"26e5073d","parentId":"44f3d1d8","timestamp":"2026-07-16T09:58:13.117Z","message":{"role":"toolResult","toolCallId":"call_lROWUje4oOcQ680ru0Nfz2oz|fc_0698befac979932d016a58ab349c18819a9d27a68d15884a53","toolName":"grep","content":[{"type":"text","text":" 151|/// discipline applied to the network (D4b): the stream's single read pump is\n*152|/// the only producer; a brain connection attaches as the subscriber; every\n*153|/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n 154|/// replay can never interleave out of `seq` order. Chunks produced while no\n 155|/// brain is attached sit in the ring and replay on the next subscribe — a\n 156|/// brain restart loses nothing (Spike #3 invariant B/D).\n 157|// [impl->REQ-DAEMON-2]\n*158|struct StreamLog {\n 159|    stream_id: u64,\n 160|    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n 161|    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n...\n 170|    cap_chunks: usize,\n*171|    subscriber: Option<SharedSend>,\n 172|    /// Read side finished cleanly (peer called finish / conn ended).\n 173|    finished: bool,\n 174|    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n...\n 194|            cap_chunks,\n*195|            subscriber: None,\n 196|            finished: false,\n 197|            retentive_cap: 0,\n 198|        }\n...\n 206|    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n*207|    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n 208|    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n 209|    /// never evicts — bytes leave only via [`drain`](Self::drain).\n 210|    fn append(&mut self, bytes: &[u8]) {\n...\n 218|        }\n*219|        if let Some(sub) = &self.subscriber {\n 220|            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n 221|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*222|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 223|            // the deadline instead of parking this producer.\n 224|            let _ = sub.write(&frame);\n 225|        }\n...\n 242|\n*243|    /// Mark the read side finished and tell the subscriber (with the final\n 244|    /// cursor, so it can verify completeness).\n 245|    fn finish(&mut self) {\n 246|        self.finished = true;\n*247|        if let Some(sub) = &self.subscriber {\n 248|            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n 249|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*250|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 251|            // the deadline instead of parking this producer.\n 252|            let _ = sub.write(&frame);\n 253|        }\n...\n 255|\n*256|    /// Attach `sub` as the subscriber, replaying every retained chunk with\n 257|    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n*258|    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n*259|        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n 260|        // record — this is one of the two RCA-pinned shared-carrier classes.\n 261|        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n*262|        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n 263|        sub.lifecycle_event(\n 264|            \"stream-sub-attach\",\n 265|            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n 266|        );\n*267|        self.subscriber = Some(Arc::clone(&sub));\n 268|        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n 269|            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n 270|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*271|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 272|            // the deadline instead of parking this producer.\n 273|            let _ = sub.write(&frame);\n 274|        }\n...\n 277|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*278|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 279|            // the deadline instead of parking this producer.\n 280|            let _ = sub.write(&frame);\n 281|        }\n...\n 283|\n*284|    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n 285|    /// not unsubscribe a newer brain).\n 286|    fn detach_if(&mut self, sub: &SharedSend) {\n*287|        if let Some(cur) = &self.subscriber {\n 288|            if Arc::ptr_eq(cur, sub) {\n*289|                self.subscriber = None;\n 290|            }\n 291|        }\n 292|    }\n...\n 305|    cap_events: usize,\n*306|    subscriber: Option<SharedSend>,\n 307|}\n 308|\n 309|impl PresenceLog {\n...\n 314|            cap_events,\n*315|            subscriber: None,\n 316|        }\n 317|    }\n 318|\n 319|    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n*320|    /// ring, live-send to the current subscriber. Producers: `register_conn`\n 321|    /// (connected) and the per-conn closed-watchers (disconnected) — every\n*322|    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n 323|    /// in strict `seq` order.\n 324|    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n 325|        self.push(NetPresenceEvent {\n...\n 366|    /// Assign the next `seq`, retain in the bounded ring, live-send to the\n*367|    /// current subscriber. Every append serializes on the enclosing `Mutex`, so\n*368|    /// subscriber frames arrive in strict `seq` order.\n 369|    fn push(&mut self, mut ev: NetPresenceEvent) {\n 370|        ev.seq = self.next_seq;\n 371|        self.next_seq += 1;\n...\n 375|        }\n*376|        if let Some(sub) = &self.subscriber {\n 377|            let frame = net_presence_event_envelope(&ev);\n 378|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*379|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 380|            // the deadline instead of parking this producer.\n 381|            let _ = sub.write(&frame);\n 382|        }\n...\n 384|\n*385|    /// Attach `sub` as the subscriber, replaying every retained event with\n 386|    /// `seq >= from_seq` before live streaming (the gapless-resume attach).\n*387|    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n*388|        // Attribution (F-039 legs b+d): presence-subscriber role fact + attach\n 389|        // record — the OTHER RCA-pinned shared carrier (the per-dial-failure\n 390|        // presence push rides this conn). [impl->REQ-CONN-POISON-ATTRIBUTION]\n*391|        sub.describe(\"presence-subscriber\");\n 392|        sub.lifecycle_event(\"presence-sub-attach\", &format!(\"from_seq={from_seq}\"));\n*393|        self.subscriber = Some(Arc::clone(&sub));\n 394|        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {\n 395|            let frame = net_presence_event_envelope(ev);\n 396|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*397|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 398|            // the deadline instead of parking this producer.\n 399|            let _ = sub.write(&frame);\n 400|        }\n...\n 402|\n*403|    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n 404|    /// not unsubscribe a newer brain).\n 405|    fn detach_if(&mut self, sub: &SharedSend) {\n*406|        if let Some(cur) = &self.subscriber {\n 407|            if Arc::ptr_eq(cur, sub) {\n*408|                self.subscriber = None;\n 409|            }\n 410|        }\n 411|    }\n...\n 448|/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n*449|/// Both append to the same [`StreamLog`], so the subscriber (and everything\n 450|/// above it) cannot tell the transports apart.\n 451|enum RecvHalf {\n 452|    Quic(RecvStream),\n...\n 1499|\n*1500|    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n 1501|    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n 1502|    // [impl->REQ-DAEMON-2]\n*1503|    pub(crate) fn subscribe_stream(\n 1504|        &self,\n 1505|        stream_id: u64,\n 1506|        sub: SharedSend,\n...\n 1522|\n*1523|    /// Detach `sub` from a stream iff it is the current subscriber (brain\n 1524|    /// disconnect cleanup; the stream + its ring persist).\n*1525|    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n 1526|        let log = {\n 1527|            let streams = self.shared.streams.lock().unwrap();\n 1528|            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n...\n 1534|\n*1535|    /// Attach `sub` as the presence subscriber, replaying liveness events from\n 1536|    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.\n 1537|    // [impl->REQ-EP-4]\n 1538|    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n...\n 1541|\n*1542|    /// Detach `sub` from presence iff it is the current subscriber (brain\n 1543|    /// disconnect cleanup; the log + its ring persist).\n 1544|    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {\n 1545|        self.shared.presence.lock().unwrap().detach_if(sub);"}],"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":45,"fileCount":1,"files":["crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":45}],"truncated":false,"displayContent":"  151│/// discipline applied to the network (D4b): the stream's single read pump is\n *152│/// the only producer; a brain connection attaches as the subscriber; every\n *153│/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n  154│/// replay can never interleave out of `seq` order. Chunks produced while no\n  155│/// brain is attached sit in the ring and replay on the next subscribe — a\n  156│/// brain restart loses nothing (Spike #3 invariant B/D).\n  157│// [impl->REQ-DAEMON-2]\n *158│struct StreamLog {\n  159│    stream_id: u64,\n  160│    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n  161│    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n     │...\n  170│    cap_chunks: usize,\n *171│    subscriber: Option<SharedSend>,\n  172│    /// Read side finished cleanly (peer called finish / conn ended).\n  173│    finished: bool,\n  174│    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n     │...\n  194│            cap_chunks,\n *195│            subscriber: None,\n  196│            finished: false,\n  197│            retentive_cap: 0,\n  198│        }\n     │...\n  206│    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n *207│    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n  208│    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n  209│    /// never evicts — bytes leave only via [`drain`](Self::drain).\n  210│    fn append(&mut self, bytes: &[u8]) {\n     │...\n  218│        }\n *219│        if let Some(sub) = &self.subscriber {\n  220│            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n  221│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *222│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  223│            // the deadline instead of parking this producer.\n  224│            let _ = sub.write(&frame);\n  225│        }\n     │...\n  242│\n *243│    /// Mark the read side finished and tell the subscriber (with the final\n  244│    /// cursor, so it can verify completeness).\n  245│    fn finish(&mut self) {\n  246│        self.finished = true;\n *247│        if let Some(sub) = &self.subscriber {\n  248│            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n  249│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *250│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  251│            // the deadline instead of parking this producer.\n  252│            let _ = sub.write(&frame);\n  253│        }\n     │...\n  255│\n *256│    /// Attach `sub` as the subscriber, replaying every retained chunk with\n  257│    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n *258│    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n *259│        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n  260│        // record — this is one of the two RCA-pinned shared-carrier classes.\n  261│        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n *262│        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n  263│        sub.lifecycle_event(\n  264│            \"stream-sub-attach\",\n  265│            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n  266│        );\n *267│        self.subscriber = Some(Arc::clone(&sub));\n  268│        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n  269│            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n  270│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *271│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  272│            // the deadline instead of parking this producer.\n  273│            let _ = sub.write(&frame);\n  274│        }\n     │...\n  277│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *278│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  279│            // the deadline instead of parking this producer.\n  280│            let _ = sub.write(&frame);\n  281│        }\n     │...\n  283│\n *284│    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n  285│    /// not unsubscribe a newer brain).\n  286│    fn detach_if(&mut self, sub: &SharedSend) {\n *287│        if let Some(cur) = &self.subscriber {\n  288│            if Arc::ptr_eq(cur, sub) {\n *289│                self.subscriber = None;\n  290│            }\n  291│        }\n  292│    }\n     │...\n  305│    cap_events: usize,\n *306│    subscriber: Option<SharedSend>,\n  307│}\n  308│\n  309│impl PresenceLog {\n     │...\n  314│            cap_events,\n *315│            subscriber: None,\n  316│        }\n  317│    }\n  318│\n  319│    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n *320│    /// ring, live-send to the current subscriber. Producers: `register_conn`\n  321│    /// (connected) and the per-conn closed-watchers (disconnected) — every\n *322│    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n  323│    /// in strict `seq` order.\n  324│    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n  325│        self.push(NetPresenceEvent {\n     │...\n  366│    /// Assign the next `seq`, retain in the bounded ring, live-send to the\n *367│    /// current subscriber. Every append serializes on the enclosing `Mutex`, so\n *368│    /// subscriber frames arrive in strict `seq` order.\n  369│    fn push(&mut self, mut ev: NetPresenceEvent) {\n  370│        ev.seq = self.next_seq;\n  371│        self.next_seq += 1;\n     │...\n  375│        }\n *376│        if let Some(sub) = &self.subscriber {\n  377│            let frame = net_presence_event_envelope(&ev);\n  378│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *379│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  380│            // the deadline instead of parking this producer.\n  381│            let _ = sub.write(&frame);\n  382│        }\n     │...\n  384│\n *385│    /// Attach `sub` as the subscriber, replaying every retained event with\n  386│    /// `seq >= from_seq` before live streaming (the gapless-resume attach).\n *387│    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n *388│        // Attribution (F-039 legs b+d): presence-subscriber role fact + attach\n  389│        // record — the OTHER RCA-pinned shared carrier (the per-dial-failure\n  390│        // presence push rides this conn). [impl->REQ-CONN-POISON-ATTRIBUTION]\n *391│        sub.describe(\"presence-subscriber\");\n  392│        sub.lifecycle_event(\"presence-sub-attach\", &format!(\"from_seq={from_seq}\"));\n *393│        self.subscriber = Some(Arc::clone(&sub));\n  394│        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {\n  395│            let frame = net_presence_event_envelope(ev);\n  396│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *397│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  398│            // the deadline instead of parking this producer.\n  399│            let _ = sub.write(&frame);\n  400│        }\n     │...\n  402│\n *403│    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n  404│    /// not unsubscribe a newer brain).\n  405│    fn detach_if(&mut self, sub: &SharedSend) {\n *406│        if let Some(cur) = &self.subscriber {\n  407│            if Arc::ptr_eq(cur, sub) {\n *408│                self.subscriber = None;\n  409│            }\n  410│        }\n  411│    }\n     │...\n  448│/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n *449│/// Both append to the same [`StreamLog`], so the subscriber (and everything\n  450│/// above it) cannot tell the transports apart.\n  451│enum RecvHalf {\n  452│    Quic(RecvStream),\n     │...\n 1499│\n*1500│    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n 1501│    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n 1502│    // [impl->REQ-DAEMON-2]\n*1503│    pub(crate) fn subscribe_stream(\n 1504│        &self,\n 1505│        stream_id: u64,\n 1506│        sub: SharedSend,\n     │...\n 1522│\n*1523│    /// Detach `sub` from a stream iff it is the current subscriber (brain\n 1524│    /// disconnect cleanup; the stream + its ring persist).\n*1525│    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n 1526│        let log = {\n 1527│            let streams = self.shared.streams.lock().unwrap();\n 1528│            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n     │...\n 1534│\n*1535│    /// Attach `sub` as the presence subscriber, replaying liveness events from\n 1536│    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.\n 1537│    // [impl->REQ-EP-4]\n 1538│    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n     │...\n 1541│\n*1542│    /// Detach `sub` from presence iff it is the current subscriber (brain\n 1543│    /// disconnect cleanup; the log + its ring persist).\n 1544│    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {\n 1545│        self.shared.presence.lock().unwrap().detach_if(sub);"},"isError":false,"timestamp":1784195893117}}
{"type":"message","id":"419e4128","parentId":"26e5073d","timestamp":"2026-07-16T09:58:13.118Z","message":{"role":"toolResult","toolCallId":"call_1YCdPxqZXa44uN8izGI00Icp|fc_0698befac979932d016a58ab349c08819a811ce865b1a267fc","toolName":"grep","content":[{"type":"text","text":" 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 45|//!\n*46|//! Each claimed stream gets its own worker thread with its **own** brain IPC\n 47|//! connection (the broker is multi-client by design; the probe-brain pattern\n 48|//! every E2E test uses). The peek and the serve use **separate** connections:\n 49|//! the peek brain subscribes from 0 to read the first line and is dropped;\n...\n 56|\n*57|use std::collections::HashSet;\n 58|use std::io;\n 59|use std::path::PathBuf;\n 60|use std::sync::atomic::{AtomicBool, Ordering};\n...\n 205|    let mut brain = connect(broker_name)?;\n*206|    let mut claimed: HashSet<u64> = HashSet::new();\n 207|\n 208|    while !stop.load(Ordering::Relaxed) {\n 209|        let reply = brain.net_streams()?;\n 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...\n 282|\n*283|/// One claimed stream's worker: peek → classify → run the family's serve\n 284|/// loop on a fresh brain connection.\n 285|fn worker(\n 286|    broker_name: &str,\n...\n 390|                None => {\n*391|                    // Refuse politely: finish our send side, claim stays.\n 392|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 393|                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394|                }\n...\n 463|/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n*464|/// instance of an id observed transitioning to `Active` — its node's daemon\n 465|/// just gave it a driver, so a *locally hosted* instance of the same id\n 466|/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n 467|/// dormant). The rest edge's transition echo is gate-armed\n...\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...\n 736|        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n*737|            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n 738|        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 739|    }\n 740|"}],"details":{"scopePath":"crates/spt-daemon/src/dispatch.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":12,"fileCount":1,"files":["crates/spt-daemon/src/dispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/dispatch.rs","count":12}],"truncated":false,"displayContent":"  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  45│//!\n *46│//! Each claimed stream gets its own worker thread with its **own** brain IPC\n  47│//! connection (the broker is multi-client by design; the probe-brain pattern\n  48│//! every E2E test uses). The peek and the serve use **separate** connections:\n  49│//! the peek brain subscribes from 0 to read the first line and is dropped;\n    │...\n  56│\n *57│use std::collections::HashSet;\n  58│use std::io;\n  59│use std::path::PathBuf;\n  60│use std::sync::atomic::{AtomicBool, Ordering};\n    │...\n 205│    let mut brain = connect(broker_name)?;\n*206│    let mut claimed: HashSet<u64> = HashSet::new();\n 207│\n 208│    while !stop.load(Ordering::Relaxed) {\n 209│        let reply = brain.net_streams()?;\n 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    │...\n 282│\n*283│/// One claimed stream's worker: peek → classify → run the family's serve\n 284│/// loop on a fresh brain connection.\n 285│fn worker(\n 286│    broker_name: &str,\n    │...\n 390│                None => {\n*391│                    // Refuse politely: finish our send side, claim stays.\n 392│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 393│                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394│                }\n    │...\n 463│/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n*464│/// instance of an id observed transitioning to `Active` — its node's daemon\n 465│/// just gave it a driver, so a *locally hosted* instance of the same id\n 466│/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n 467│/// dormant). The rest edge's transition echo is gate-armed\n    │...\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    │...\n 736│        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n*737│            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n 738│        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 739│    }\n 740│"},"isError":false,"timestamp":1784195893118}}
{"type":"message","id":"521f47e6","parentId":"419e4128","timestamp":"2026-07-16T09:58:13.124Z","message":{"role":"toolResult","toolCallId":"call_RR65JwrV2GswhZRNKAWpdBY7|fc_0698befac979932d016a58ab349be4819a9b45e2b4f2399c32","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…\n            }\n        }\n…\n    }\n}\n\n[Showing lines 1-318 of 759. Use :319 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","truncated":true,"truncatedBy":"lines","totalLines":759,"totalBytes":13265,"outputLines":318,"outputBytes":13265,"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            ) {\n…\n            }\n        }\n…\n    }\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,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,null,322,323,null,456,457]},"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":13265,"outputLines":318,"outputBytes":13265,"shownRange":{"start":1,"end":318},"nextOffset":319}}},"isError":false,"timestamp":1784195893124}}
{"type":"message","id":"a3611c2e","parentId":"521f47e6","timestamp":"2026-07-16T09:58:13.137Z","message":{"role":"toolResult","toolCallId":"call_TK9xU219ICGCpmWgzM7ZQkQN|fc_0698befac979932d016a58ab349bfc819a8f1acecdb74dab68","toolName":"read","content":[{"type":"text","text":"fn broker_spawns_the_pty_child_in_the_requested_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    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…\n}\n\n[Showing lines 759-918 of 1550. Use :919 to continue]"}],"details":{"truncation":{"content":"    brain.attach(sid, 0).expect(\"attach to read output\");\r\n    let mut out = Vec::new();\r\n    brain\r\n        .read_output_until(leaf.as_bytes(), &mut out)\r\n        .expect(\"child prints its cwd\");\r\n    assert!(\r\n        count(&out, leaf.as_bytes()) >= 1,\r\n        \"the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n    brain.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\r\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\r\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\r\n// dedup through the journals, input B sent INTO the dead window arrives, and\r\n// B's viewport holds every marker exactly once.\r\n// [unit->REQ-INST-8]\r\n#[test]\r\nfn attach_survives_target_brain_restart_exactly_once() {\r\n    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","truncated":true,"truncatedBy":"lines","totalLines":792,"totalBytes":7668,"outputLines":160,"outputBytes":7668,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    brain.attach(sid, 0).expect(\"attach to read output\");\r\n    let mut out = Vec::new();\r\n    brain\r\n        .read_output_until(leaf.as_bytes(), &mut out)\r\n        .expect(\"child prints its cwd\");\r\n    assert!(\r\n        count(&out, leaf.as_bytes()) >= 1,\r\n        \"the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n    brain.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\r\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\r\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\r\n// dedup through the journals, input B sent INTO the dead window arrives, and\r\n// B's viewport holds every marker exactly once.\r\n// [unit->REQ-INST-8]\r\n#[test]\r\nfn attach_survives_target_brain_restart_exactly_once() {\r\n    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\");\n…\n}","startLine":721,"lineNumbers":[721,null,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,null,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":7668,"outputLines":160,"outputBytes":7668,"shownRange":{"start":759,"end":918},"nextOffset":919}}},"isError":false,"timestamp":1784195893137}}
{"type":"message","id":"97dd3d13","parentId":"a3611c2e","timestamp":"2026-07-16T09:58:13.150Z","message":{"role":"toolResult","toolCallId":"call_jCbFCEXp0iWHzlYISSDhfq6I|fc_0698befac979932d016a58ab349c20819aba2fe9c6cf739e39","toolName":"read","content":[{"type":"text","text":"fn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n…\n    if !base.exists() {\n        return base;\n    }\n    for n in 1u64.. {\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\n        if !candidate.exists() {\n            return candidate;\n        }\n    }\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\n}\n\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n/// config are re-read each sweep (adapter / pulse-period changes land between\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n/// for symmetry — the threads die with the brain process and the next brain\n/// rehydrates from disk.\n// [impl->REQ-DAEMON-1]\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(LiveSet::new());\n        {\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\n            // residency machinery's corpse-burial, not its resurrection.\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\n            // running FROM the own-copy image is killed first (its dir then unlocks).\n            gc_live_bin_dirs(&owlery);\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n…\n        }\n    })\n}\n\n[Showing lines 959-1023 of 2157. Use :1024 to continue]"}],"details":{"truncation":{"content":"    if !base.exists() {\r\n        return base;\r\n    }\r\n    for n in 1u64.. {\r\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\r\n        if !candidate.exists() {\r\n            return candidate;\r\n        }\r\n    }\r\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\r\n}\r\n\r\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\r\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\r\n/// config are re-read each sweep (adapter / pulse-period changes land between\r\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\r\n/// for symmetry — the threads die with the brain process and the next brain\r\n/// rehydrates from disk.\r\n// [impl->REQ-DAEMON-1]\r\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        let set = Arc::new(LiveSet::new());\r\n        {\r\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\r\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\r\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\r\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\r\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\r\n            // residency machinery's corpse-burial, not its resurrection.\r\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\r\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\r\n            // running FROM the own-copy image is killed first (its dir then unlocks).\r\n            gc_live_bin_dirs(&owlery);\r\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\r\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\r\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\r\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\r\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\r\n            // (its sessions are live, not orphaned). Belts inside the gate.\r\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n        }\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\r\n            // unset = normal production, untouched): skip the reconcile poll+body so an\r\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\r\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\r\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\r\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\r\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\r\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\r\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\r\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\r\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\r\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\r\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\r\n                std::thread::sleep(Duration::from_millis(100));\r\n                continue;\r\n            }\r\n            let owlery = perch::owlery_dir();\r","truncated":true,"truncatedBy":"lines","totalLines":1199,"totalBytes":4130,"outputLines":65,"outputBytes":4130,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n…\n    if !base.exists() {\r\n        return base;\r\n    }\r\n    for n in 1u64.. {\r\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\r\n        if !candidate.exists() {\r\n            return candidate;\r\n        }\r\n    }\r\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\r\n}\r\n\r\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\r\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\r\n/// config are re-read each sweep (adapter / pulse-period changes land between\r\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\r\n/// for symmetry — the threads die with the brain process and the next brain\r\n/// rehydrates from disk.\r\n// [impl->REQ-DAEMON-1]\r\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        let set = Arc::new(LiveSet::new());\r\n        {\r\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\r\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\r\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\r\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\r\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\r\n            // residency machinery's corpse-burial, not its resurrection.\r\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\r\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\r\n            // running FROM the own-copy image is killed first (its dir then unlocks).\r\n            gc_live_bin_dirs(&owlery);\r\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\r\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\r\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\r\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\r\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\r\n            // (its sessions are live, not orphaned). Belts inside the gate.\r\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n        }\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\r\n            // unset = normal production, untouched): skip the reconcile poll+body so an\r\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\r\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\r\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\r\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\r\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\r\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\r\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\r\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\r\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\r\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\r\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\r\n                std::thread::sleep(Duration::from_millis(100));\r\n                continue;\r\n            }\r\n            let owlery = perch::owlery_dir();\r\n…\n        }\n    })\n}","startLine":957,"lineNumbers":[957,null,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,null,1054,1055,1056]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\livehost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2157,"totalBytes":4130,"outputLines":65,"outputBytes":4130,"shownRange":{"start":959,"end":1023},"nextOffset":1024}}},"isError":false,"timestamp":1784195893150}}
{"type":"message","id":"09b3dfdd","parentId":"97dd3d13","timestamp":"2026-07-16T09:58:54.406Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming redispatch lifecycle race**\n\n**Analyzing dispatcher reconnect race conditions**\n\n**Clarifying controlled PTYs ownership**"},{"type":"thinking","thinking":"**Analyzing process kill behavior and handle inheritance**\n\n**Investigating handle inheritance in process spawning**"},{"type":"toolCall","id":"call_cPGOTjCCikURUm8JygjNjkbR|fc_0698befac979932d016a58ab5d54b8819a840166c84d6de33a","name":"grep","arguments":{"i":"Finding brain descendant spawns","pattern":"Command::new|spawn_detached|detached_no_inherit|spawn_deelevated|CREATE_PROCESS|run_bounded_command|launch_harness","path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/shellwake.rs;crates/spt-daemon/src/pump;crates/spt-daemon/src/brainproc.rs;crates/spt-runtime/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_KocN4Tj5sUjWKwrFoMCxGsQP|fc_0698befac979932d016a58ab5d55a8819aae18d6026ef54d7a","name":"read","arguments":{"i":"Reading inherited handle hazards","path":"docs/KNOWN-HAZARDS.md","selector":"205-250,325-370"}},{"type":"toolCall","id":"call_VurTmSy9tPCYvzlsW53XKj8R|fc_0698befac979932d016a58ab5d55bc819a961abb30d5c5d108","name":"grep","arguments":{"i":"Finding no inherit primitive callers","pattern":"detached_no_inherit|bInheritHandles|inherit_handles|handle inheritance|NO_INHERIT|Stdio::null","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_oe1tuDY6Jj0IsdiMU6lShS9J|fc_0698befac979932d016a58ab5d55c8819a87d1b2c816550798","name":"grep","arguments":{"i":"Finding psyche process launch","pattern":"spawn_psyche|psyche.*spawn|launch_.*psyche|Child::spawn|role.*spawn","path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11929,"output":1205,"cacheRead":151040,"cacheWrite":0,"totalTokens":164174,"reasoningTokens":863,"cost":{"input":0.059645000000000004,"output":0.03615,"cacheRead":0.07551999999999999,"cacheWrite":0,"total":0.171315}},"stopReason":"toolUse","timestamp":1784195893159,"responseId":"resp_0698befac979932d016a58ab357da0819a9737cf7a946d9311","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ab38e87c819a85c3c94848ade05c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtMzR9ESSeqHRVpgEq7FxswVIPlUHF5f2XqNFT3HD7rfybATebEGXdH5B-WH91WicgAY9JXpqOn5nMH-pllFsB3SHDnE9x079RddTsiWjRfQQFWAwZEontrnVMC40DDkV3qGrFHLcjsdDkJsTapvY3-vZgs5IWkIBUTWQ5uRpnE7VGwZJP48ysb-O-ewh4oEk0Zw4yBWAShHDGOLVj-_ahANW_ZUVu4ZxMmUW_n_hzQoCb8l0uckJQiBsS8lMAJDOn5fzpRuxqUv1UVYm1Ab7WmELI-L6Bx1fTV0b_s9yBgjydD0D4tIwYgV7R-_x6nX9arF509Py_3WijHHx8uX0vnirJHHXbv4aj0m8mMip4jb4RwPwC89e_yUEJ3KV_1HUhLgMpl79zWWhgs7IJMro3ceDePQQjB9EA4GvANeev-QN94Oyd3Kl_VXuRNnCS3Md3jPQEpTiGk70UYPcmqPXFpvvwDwAvXHQM050mq3Bs_fyISyk2wCgSKQEumY_zBxLOI3T42idMgPkoMFDP1yVU3RGtQk7gwT94oYKhzVT5mJpJt-fcvttTRveAroOpQY28T4seurLGHiaXuYLj5UUgAaXr-kBrF_4ylCdJUOwHX0qpMBjEP0uog_QZ8v9KXilWi3OLMrngjl8BYG9DFkxne0ePdqlLt2drEiuk_dsx2SokrZVIu7iTeeDvqwowXpsrZh5flJokiA9jkTDBVugG0Temg09_1U-EfZq-aZtw9wksoYqOxA9pdEUduy9p6DA9tb6P4NkDjRHlI1CzgeNAoIBlMYZFHTaK4FGmwGPy59DXV_8DcLBnIAIZEId9zogaLw6X9oHGP-liv7LuqXCL0C5AlVkJjF5z9VkSxHy40YWoNV4G_jJ3C1_UOif3SEpVGfg9lKZRJR4B4jIBSThkDqAUoFKsBdF6et7TuEUH6D6-EPXdG55K0dznm1twmn4_Gpxvp6h7ZOu0MkRm4vXys8m1ZeBsidkrS2LPS039X5pnDVyf330mJAm3yvMXD-XohAwLOgMCA3LF3sJJ9WMS2K1ke1uY-6x8FMudIs3gjmCGaqrcdlCnfwj3i3QR-pfN6jHrLIEaj_MD3EluRtH3Kms7G5iOK-_4uHh2U3amTDTz-My0vkfkFahPIPB8Igij53f098W82wMfRe3rMBf2BuOW9jCBb8h_k6B0oeZI_7NBn1bGSfH3fTydoLM7Q9-oBB_z2zgJb7nazN5FicVlc3GSUoscXSErnfWNvAMHPc7OIlti_saOuJhSvRp2x8BHTQRJmqszd89e45SpAk6dD4FOCU41xBrGEV_eS8yR2e4pNDnQDfp0BnWaPciOQ3SBq21_S27F2Qv8CRvOUMrc6nPsyU-kp25LNFdyNJqGZ6X90x6nM5wEOzEKvJtkSgD-oHww-ZKCjZVFV7dbvZEdb3N-FktQBePzNV6HTMJ-7VRrp9DIxxbkETIqaZSfFqzVX05hAodr5Nmz_EWSU9f6E_I866Xh7jm3P1VDpxC4qbIFjtYmBX4JZffmWZsh746spBPe4Cmzuh6DstNh1oxmVeQ3VBuSY4vnqc5vAEN8HbQ7HkFw5AV94UaFC0O5-_Bvtn7kZgRbmy_HzWnAcIYaHX-DMnXludXoMpRJiaYFS4s5e0K-k01rT7-UTpcx5labsHsG_Arb5qv6-6CoTYT96Uy6jPZdqlxSwFaxR5EIp31ZyQJvgH6D9o7EmcT37PyOV1Zxf8aSFwLUb4I7Ve6lMxd5n-5vAoyizgkSEhCqrrv8osjk-NtAM4ayT5ExWoQ0N7cnwJtaNri63NUU2rJ_zz0jXqQf7CeUbbvuwSi8zLT7tuZwu2C0ipaDIKW8abZg36KJXFXaS_ZaIwTdTi_nJHc-KGAYK59BRLSdZYwe_T7OWD0EihJG0wPv1PDYK2fgmHVd9vGJrUbvPlK7U7yKBG5o2IChsiPnB-4EOR6K3Day-ucNJR1_YUNS9Bt7mmB057OP3HJcS3mk5A7YEHkQhj5fRHFiuqIhGJl0Z4U2fiCaaEc50nFx3ysTwPsMhduuxIIxckjlzxKDwx7vjESRXqSOh4y9HVRBw_8gfV_22LX2ypd3vAhqnxbo_1wWUJb8yybiZdJNRlzqYRMY0BznJVvwCp3USgW5eFAqhWzPI3nmgkNoE68gRcolN6uwgPqvwAHWJU3aWXU8CZfEQO3t22qF9pTWXC2zbP7I6_kBhB21gndM3mjSO0xzjpQcHrxAezqGU-ffLSmyQfZjiRhU7FvHgZETj_V-UAWVJ0hP6hIctSq4g037_CMJBxSFCLsn5mQNID1rOJrPXHU0IJsDxsFZGhjfkl1z7C05FmeEWSBo3HSsu-aziyppeoBDZ2LG23XybOAI2fl4QWlbi99tbLSFeaFTGUQVbrD16kQUHLi26HWJzCWPesA0YNcruIj0WdhNr7JBDJsLPElhxJJqim8AF9JgsF4pU85FjeZYOYyJmh4BGlBwFiwVBOQ1MbYFma5EhYcOjzHZZI5tcgVPDIpynHm-0QHkMviB4KqQngMOiawJpSszr5n2oyFSCWsx4iuXk6KGmVNZWBDfS-7lckGWW84G_7UnGP7mvqhzzZkI96fBEPbTnGLzy9O9dTY0vMWl0rBA8PXQd-NqYGor7vvuUs2h4Qxx_cGB1Q7FCw20Ml8mMIUVnXmuUNyXXIPZ44Li0mx4bU4wqckxerikTgTBmN6sqrT2mXACzlWZEpnzdTBeeO7sFGXr1SYmrNb0D-7JOq6miGuroAd7Rs8b3nWLYIC_ORgCId_dbRFnJkpUGb6i0Z2rCxttoFo1jgJ_Ki0Zyl-b--g2GGdGbdVtArTh9oaJPounfAUF_nd8Euu_vXmBkMG9XOVFmlYf_wmhaxwVQYH4-08_Eg33PUY5V7bQ196Qf4aC-WuUIqstmIkriYSufmHyu7UDZdaPbjqns07YKBV0IL8_vblomZBbzE0twdrDpEts8ub4-Ab2u5XthB8fWHwNjnNdKbbjJiDCpN6paer-Wm2ZmyGEtoSZcjdQ-K_3ZMWEe0JWmpZXmSpFEnn6xFHQ1oB83uQ3yaoxAokToa37DG7iBnahZ5FYCtIDUcAqmZUhmDYOjHs3gcEFP0h80Egkpmik9M528EY7No25hAi7NV0-Vn_hq4nWd_-SDKvC41YEp1q1ZOLBEr7nJr8v7FH8S8-YECov09O-M0gHneXujzE0Hsx3jEHDsMoD3nbpGShDe91GvAbKe1POVBWf5HzIrXwX3heclXHVTIf-L6GE4gJouek_AwQ4L753mXhXNZiQxwwENcRDLolITUCA9U4M-l0IgUhsZb3zkVBBZu0VJ5mJ6vr0BX_amr5EpNyqy1bd9wbkvlXy0zN1dbLWjbpRf2l5ho883F6Ho0DZMDDHvgOvSnvSE652_e9kInfa7SQ8rMLtW8iD87M1Lawo_MRnz6zvZmuriEEAw1aiuHDwlCoD4_vfI0r0c0JjelZgWgNKBHgi2Yel9L4j0Z_un-IPU1X4XQUvNed3WTfbKSgygeW9-_yHlEoHU-KlIejTZgk8BlamCyS4wM9g1JEEmQjaGziUochz_MpDatdmtiLFNuTcizyVMM3zb4a_DZF1fUCSzlXD3bEtiK1zo36LAYYgFqD8HfnIRVV5NYeRMSbHUKoJgKjCEwrBYAixwbgEbDdUSkN6RHJlo9EKQXFJI8TsVsKFFCsVH4_1mAG6SDnOtNNkNgmUyRppd3fsGKOBRRjVskO9Np9v6kX6t7SS_r2W8VQs4vCktluxSXerdkkWdX6xB2anVG_aeRzNp_5f1aqjuqBkWbxk0p2HYY3NQixUa0QIv-2AruCBAi2H_cXeap4pwNfLEgMIhuGoffo0C3_gBa23plNTyK6ucFuIFx7QHZKSWj0yRCUPYNdqhp50xW5fVsVSzeND9hcbErA1FizuuxJtIisERIRMrNChl3H7hjIlHT2B4BNbqdGvkA-qwK6kVeeTFR24scLGBicE3s-SYBBJ4Jy99npq8FTfQuvmYt7oHAjuMQtsLoWYo40g4tUU34wlnImFR6Cf-d_gD2XTCuvQin0GsBup2mdNE1hHpHZPOOuqpKYqb9SDIsP3IEqDc3UrRyOS8whUNavNDPFFbrHjxbkmNUusuOqB1MDOYHGN2KdxbPnFWEXsTWCqSGSG50RZPLFaypgcioHLiGiF19Mgd5ZrZP1nRIrcP9dWAxBLahAS3G6-9Eqt29T5pY67qThAFHB-X_LMo48Dq8Cx2eg7BF0UO_vvo683PaKOeYIix7BzbOKLWoAa2A6GImRA-fNTJM849-KZvepve7CK9B0_BRn-PercrJmcZVPdBZa4Wd64BJa5hQEb55rQEOBp7hMFkFMtjToGXc5nXNd1F8XdeGDczkkVq0Kps9ksCFNuxlkvPkC86","summary":[{"type":"summary_text","text":"**Confirming redispatch lifecycle race**"},{"type":"summary_text","text":"**Analyzing dispatcher reconnect race conditions**"},{"type":"summary_text","text":"**Clarifying controlled PTYs ownership**"}]},{"id":"rs_0698befac979932d016a58ab4c2c44819aa13ae625c285fa07","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtdBr7f_-zK69I0KX8kh8staE_x8jHYMSx8YYuw75oVnweuHPYPhCqdhM-xPuYK-H_gLQMR4xGrLolIaWtK1E6-qso27xQfP8XjkRNEjEbQ9RJ0bg0XoKrW0I38d7jjdgCtqTTVjJYfKDBygMk_lrdd0pvOewl8M5oyE9Xy5M1IGwkiGPnbOXscyo8RhlS401_aJIgOm2Dc6K7hmiMXL30g82F3ghkeEVD13rJinHl-GAOE5fypEfwm5byFQ69AtJ-awERiBqVsY55jJ_E3PmgdqpH8xYKdL71cQEacg3JsX1EPZX-PYzWO_prO2Zv9_rhOR0KM6N-EkJuTM7UlM37BK6Jar0jzxCfZx_QGwY3B01-g-PX2u4t0mZg5N-fzxCfjEQmwNwwNDrJfrdJ6BjNvwnOMrr5hm13P5fbCWtVwHxSwIyW2xfVaQ6vA2VQpouKkjNmuP69ukMe6qJ_dFMnjBI27Vb6_o3KkQyMS5OcQOgFnTm4ic0d6fvor1yhCweyJlimmd7hqjtNtFnXERtMl74ESZmYxqEat1QhJ1ONFbRF35lA75gbt4c3FykAWfMUQPDZwQ9RsCCw6WlRmx2FjZBCm6viyOb5kVtqMl6IZxaOMjRizyZSbr8X68LU87wS4xomlew9NE0rWPKSAjq4VXYrErSeQuPpefDENfwjnssfWgJL8NtTF4TzAzMuiECy80-j457ZPyFirzfxsAfkJGrz7NsukTgVQ_NbVgssF13cbSjTGCp11Um-l8nc8fxXb_uqmauLXA9Np_qiKj7n8_QiHPKvt9HQBWm1qdd8qM2fsd7wkH4WYfk4WbU2QzWqyi8Xahy4OtWuWyardfLdb9_CVfbemEFXBaIUMxw7cwcJfy7m4bog0VNhGaGNwPCf0xbIsZhnfbzVs9xGhJc6gevlJbV1LrmGTLtoRdsYEBcUgOL8Fuh975OgjpzjAt_hV1QzX9viHD8tRlcixj0tLDtgHMB2tUP23BsPR3U6gX0XmFgsF3uMZlPVUpKzK0-DiQA8ENkssEGXS4KE49FXIcmSL-q8Om8RXfHKzZ2g7wlPDGLJSxJp6EItfSY7WzUAXDeJBnGskFZUGHDI9dA_rNW8Zpx30ZpONjSBYAOlJVkqOQrCb3SvC7NCiODCQUUM9Slu2INm8hdjfbxHUlc58TSmQ2VIyDrjg1lkZhf3bOgeLGafbTCNC6DwC4WmEg2D0GuEJhEUxwIUl5a9PlVYC-TKbNw3cDrkSWXpYoNk2dGyDuATRE9hIZAKve49_t32QU7ZKOZ-wYWuwFfALl6QLRsm0HcQyDWjlMCuXVAAuFFrzltuNtKd54Az7hIGzp2B-qYT2cpOie4qN5A9Q4Do5EmgaBm7Em9unyTZD30HJBI2atJCyyK0T2ASB3-XgfJ2_Iu15Uaa1BYO5BlefSB4XSIU9JVPnMh-kMADwzkj2YVxuw6la07abV-Uy_HJmvvv4_W_BGQYNz8ZxBCh807levPcFAv04LYZ-Ovjpi05YGlUZLqqK1vr_EWe2OuuI2q9Qu-M99Khha-0i5IT1zb5GIkBOG4fseCTEzHQEd6mT_uQYDZaqkc0EO9hKwQsrVr467Y6HWNwDKNjDfXBG6irugHsSseadcpk4X06ycbhAqK4d1rPgGjnCNvYyrO0n60GdA64_8VJ8nvIj80RCBJ9dtUFpriRkc_pH54rpy4FH1HQWCs4aQm91BVfYcYHOPtY14lXSMghe8KgPqxbpy4zCKJnRA0dUkznQOtyOdp6slyh5UM_6XOXclpog7pzj0L-mUwFAFNIJKCPhsKqzvvqQJfKUpyIDQr10vjlhUxjWuGmw_9Kcw_cqa8DMgJXZZRAN3hcmbBOb_g6Dpjs1ozAsX38UnhXEsOljAd6FqaXmiFwKE6dZuEvDqQ6OjPPodORKuDnn295geNbzXDkT_Op7fOzZi7MUaNWr19AffrmoxJD6E_svPDqGhqfQJ6b9HKD_qWq__tbzH6COQsmFcjw9O5K59SxR12WuoEJLaxVF2_mojF3tyNswX2GFBD7aRrUdyUAEln86HGcaxYo7XiX8f0YGTSLvssNy0IhGB5OPRUYeVo_dQb8LgBTrNTrAgK4sDMD2_XfuPXHrKlSakxTEsuTQGjQSWQFj0Z8zO-lUM1AweMDcPRnW1LvYiyHUQTjsbKwcf79yUpRLNr-f2eyg1NzFN0AIK8OCXWUCUhlFAAe1JrIX-YBtA6jN1X3k_Hlxst-XaCtB_dMJmrWTWHwVP7hSEA4NVAN6koDIYV8OhC5UDLhqLRzwvIQJosxcT4tczF5Fsvs0T1bl6F5rwZj1VJGsIZJmkYhrny0ImcPd11moij_lr_yvWj1dBDZ9kZmP8hXF4B1DYkUjA9rfWIuhjpHlwS3TVo42Z3OW70jToavP5xragqThbHex5xGq37B0U9_h_poX2w4bXsDp1hVEsnzYhQVD8yuM_5XqNsuriodQ4bUGBZgxBUmzprEI1M9YN2qEiyPl58wraUD6Bd4fd1ZP9Alwa_yuwIiiUP3fpIJaHzZVgA8EosOg2PxkQTlVWlcOpHrCSzA_1EJhvfO9mFl1w81wUXp57-Dxh_HnCHdD6zfcNGkRxP5brnx_JVSC-D8MLMh0q9TGncj-_YXN5-476fwD947JyNYLgZWlwEa6ENrHoTqf8ePH8YvfoanGJ0w7Ofo_mE9Enruql5jy0fjT5CgUDICfWdWOqsrFxoKQwqxlSW_xeMSBdHpf9gPw-Ehyu6CQY2RVqOC606Oy_fRGcFJS5K87VH7TDkt7cFPTghBnHdvJ8K-AUQ9Z9Dm5MbLKnRSTZZA1XFXIweX7iqjEqPtQfuhIZAN0c5HYnVl0auIt6qFaDpwWlxc6DO3i35m9SFXq3UX5jm7YZKhsQ4QhoaYn5rjsZRuXkZAOGTV0gfboJ8qGcxuciiB67Gkn9DeaOwF4wQvE0jMLzLhuYbNQllRxW7uaV8XSv9I_nKo9_dUC_wpdVQTLuhd3k1zixRTEE4Ap7Or21fJAPiUHVoCqMCW4ITIcgdk5SgvUVh1oXZedVv2QTzhIOvOApE-fX7wDUhiGn7PlqqvR37toFaoYp5fd3ybGXMdN4EfOJxfVCPHAeFxuX4Ec0v6b4ezY1aXcIaTI9IbH3pSeyyw5G6xuOOwHtdEDV6f4jvS8U5ih1iak0iFLSONfDT36jn-Pkb0fy01bOJRhfuInZEMLykGq0n7CEmjnTXzkXZx4r6hTLcG2q5YJHJV5UQwXCZNeQCbTHrA8D_JTNnAvfBEVc62GT3X_MdTPVrVISCbocVt3gCsHeuvvwoZuSYxq3fd2","summary":[{"type":"summary_text","text":"**Analyzing process kill behavior and handle inheritance**"},{"type":"summary_text","text":"**Investigating handle inheritance in process spawning**"}]},{"id":"fc_0698befac979932d016a58ab5d54b8819a840166c84d6de33a","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding brain descendant spawns\",\"pattern\":\"Command::new|spawn_detached|detached_no_inherit|spawn_deelevated|CREATE_PROCESS|run_bounded_command|launch_harness\",\"path\":\"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/shellwake.rs;crates/spt-daemon/src/pump;crates/spt-daemon/src/brainproc.rs;crates/spt-runtime/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_cPGOTjCCikURUm8JygjNjkbR","name":"grep"},{"id":"fc_0698befac979932d016a58ab5d55a8819aae18d6026ef54d7a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading inherited handle hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"205-250,325-370\"}","call_id":"call_KocN4Tj5sUjWKwrFoMCxGsQP","name":"read"},{"id":"fc_0698befac979932d016a58ab5d55bc819a961abb30d5c5d108","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding no inherit primitive callers\",\"pattern\":\"detached_no_inherit|bInheritHandles|inherit_handles|handle inheritance|NO_INHERIT|Stdio::null\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_VurTmSy9tPCYvzlsW53XKj8R","name":"grep"},{"id":"fc_0698befac979932d016a58ab5d55c8819a87d1b2c816550798","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding psyche process launch\",\"pattern\":\"spawn_psyche|psyche.*spawn|launch_.*psyche|Child::spawn|role.*spawn\",\"path\":\"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_oe1tuDY6Jj0IsdiMU6lShS9J","name":"grep"}]},"duration":41228.89620000031,"ttft":3728.082999999635,"contextSnapshot":{"promptTokens":162969,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cPGOTjCCikURUm8JygjNjkbR|fc_0698befac979932d016a58ab5d54b8819a840166c84d6de33a","toolName":"grep","startedAt":"2026-07-16T09:58:54.406Z","args":{"path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/shellwake.rs;crates/spt-daemon/src/pump;crates/spt-daemon/src/brainproc.rs;crates/spt-runtime/src"},"intent":"Finding brain descendant spawns"},"id":"88d85dd4","parentId":"09b3dfdd","timestamp":"2026-07-16T09:58:54.406Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KocN4Tj5sUjWKwrFoMCxGsQP|fc_0698befac979932d016a58ab5d55a8819aae18d6026ef54d7a","toolName":"read","startedAt":"2026-07-16T09:58:54.406Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading inherited handle hazards"},"id":"9c1cdcb7","parentId":"88d85dd4","timestamp":"2026-07-16T09:58:54.406Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VurTmSy9tPCYvzlsW53XKj8R|fc_0698befac979932d016a58ab5d55bc819a961abb30d5c5d108","toolName":"grep","startedAt":"2026-07-16T09:58:54.406Z","args":{"path":"crates"},"intent":"Finding no inherit primitive callers"},"id":"acb44665","parentId":"9c1cdcb7","timestamp":"2026-07-16T09:58:54.406Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_oe1tuDY6Jj0IsdiMU6lShS9J|fc_0698befac979932d016a58ab5d55c8819a87d1b2c816550798","toolName":"grep","startedAt":"2026-07-16T09:58:54.406Z","args":{"path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src"},"intent":"Finding psyche process launch"},"id":"27c1821a","parentId":"acb44665","timestamp":"2026-07-16T09:58:54.406Z"}
{"type":"message","id":"c4df40a4","parentId":"27c1821a","timestamp":"2026-07-16T09:58:54.455Z","message":{"role":"toolResult","toolCallId":"call_KocN4Tj5sUjWKwrFoMCxGsQP|fc_0698befac979932d016a58ab5d55a8819aae18d6026ef54d7a","toolName":"read","content":[{"type":"text","text":"### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\n…\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\n\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\n\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n…\n## 6. Documented regressions (non-obvious invariants)\n…\n### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old code until an unrelated restart/logon. The no-endpoint-drop self-update pillar (REQ-UPD-3, ADR-0004) is silently unrealized. Observed live 2026-06-09: `enlyzeam` ran 0.3.0 with 0.3.2 on disk for ~a day, still reproducing the bug the update fixed.\n- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/03 productionized as `int`), NOT the in-process handoff shape that masks this regression.\n- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.\n- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` (verified) + ADR-0018.\n<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->\n- **D1 (restoration skeleton, ADR-0018 Q2/Q3):** the process boundary is restored — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` child (`brainproc.rs`); the broker survives the brain dying and respawns it (proven in production topology by `crates/spt/tests/brain_split.rs`). The logic loops still run broker-side (D2 migrates them); the `int` process-level survival E2E + the in-process re-point land at D7.\n- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (`[REQ-HAZARD-BRAIN-RESPAWN-PATH]`, 6.11), fixed in v0.4.2 (respawn from the canonical path captured at broker start + a promotion bytes-gate). Seamless update is now proven on **both OSes** — Win…\n\n### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).\n- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).\n- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.\n- **D5 conformance (2026-06-10):** the new durable timing state `<spt_home>/deadline-<key>.json` (restoration D5-1) is **additive** — a rolled-back pre-D5 binary does not know the file and simply ignores it (re-phasing on its own flat-sleep cadence, the pre-D5 behavior). No existing-file schema migration, no irreversible pre-ready write → the new file is rollback-N-1-safe by construction. The thing for a future D6 guard to gate is a *migration* of this file's shape, not its introduction.\n- **D6 guard (2026-06-10, restoration D6-3):** the invariant is now **asserted**, not just noted. The pre-ready durable writes are **enumerated in one place** — `spt-daemon::PRE_READY_DURABLE_FILES` (`rollback_compat.rs`): `deadline-<key>.json` (D5, `DeadlineAnchor`), `applied-state.json` (D6-1, the two-phase `AppliedRecord`), and the generation-stamped `brain.ready` breadcrumb (D6-1b, `{pid, generation}`). A **tripwire unit test** pins each one's additive / N-1-readable contract (load-bearing field names present; an unknown extra field still deserializes), so a *non-additive* pre-ready change (renamed/removed field, or a `deny_unknown_fields`/non-tolerant shape) trips the test and forces the migration **behind ready-promotion** (or into an additive form). *…\n- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n- **Closed out (2026-06-11):** the readiness-gated auto-rollback shipped (v0.4.0) and the pre-ready durable-file registry + tripwire guard (D6-3) hold. v0.4.2 added a promotion **bytes-gate** that turns a wrong-bytes respawn into an auto-rollback rather than a false `applied` record — strengthening the rollback path the same release exercised across the fleet. No in-place schema migration has shipped, so the invariant remains **asserted, not yet exercised by a real migration** (correct — activate-don't-pre-fail); the tripwire is the wire it trips the day one is needed.\n<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\n\n### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_through` on the live-send) and then **discarded** by the waiting loop's `_ => continue`. Cursor advanced + content dropped = that chunk is gone for the downstream consumer and the broker will **not** re-send it (resume reads from the delivered cursor, ADR-0018 D4)…\n- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead code, activate-don't-pre-fail).\n- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.\n\n### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same constraint that moved session continuity to the broker in D4).\n- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The update-vs-crash decision is the D3 spawn-time `StartReason`. **One-shot** (alarm) deadlines persist their absolute `target-time` at creation and **never reset** on any restart (\"remind me at 3pm\" is a commitment) — the asymmetry vs the periodic crash-reset is the rul…\n- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired helper, the port builds the *scheduler*.\n- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).\n\n### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, and the daemon records `applied:N` while still running the previous version. New code does not run; the record is optimistically wrong (the enlyzeam-class record/reality divergence, now provable via `exe_hash`). **Windows** dodged it — `GetModuleFileName` returns…\n- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof the new bytes run). If either hash is absent the gate degrades to readiness-only (N-1-safe for pre-metadata releases / a missing breadcrumb) but emits `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.\n- **Origin:** ADR-0018 Q3 silently assumed `current_exe()` path-string semantics; surfaced by the v0.4.1 fleet-roll `exe_hash` bytes assert (agents `todlando` + `doyle` + `deployah`, 2026-06-11). ADR-0018 Q3 amended.\n<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\n\n---\n\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\n\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\n\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\n…\n\n…\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\n…\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\n\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\n\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n…\n## 6. Documented regressions (non-obvious invariants)\n…\n### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old code until an unrelated restart/logon. The no-endpoint-drop self-update pillar (REQ-UPD-3, ADR-0004) is silently unrealized. Observed live 2026-06-09: `enlyzeam` ran 0.3.0 with 0.3.2 on disk for ~a day, still reproducing the bug the update fixed.\n- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/03 productionized as `int`), NOT the in-process handoff shape that masks this regression.\n- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.\n- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` (verified) + ADR-0018.\n<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->\n- **D1 (restoration skeleton, ADR-0018 Q2/Q3):** the process boundary is restored — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` child (`brainproc.rs`); the broker survives the brain dying and respawns it (proven in production topology by `crates/spt/tests/brain_split.rs`). The logic loops still run broker-side (D2 migrates them); the `int` process-level survival E2E + the in-process re-point land at D7.\n- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (`[REQ-HAZARD-BRAIN-RESPAWN-PATH]`, 6.11), fixed in v0.4.2 (respawn from the canonical path captured at broker start + a promotion bytes-gate). Seamless update is now proven on **both OSes** — Win…\n\n### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).\n- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).\n- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.\n- **D5 conformance (2026-06-10):** the new durable timing state `<spt_home>/deadline-<key>.json` (restoration D5-1) is **additive** — a rolled-back pre-D5 binary does not know the file and simply ignores it (re-phasing on its own flat-sleep cadence, the pre-D5 behavior). No existing-file schema migration, no irreversible pre-ready write → the new file is rollback-N-1-safe by construction. The thing for a future D6 guard to gate is a *migration* of this file's shape, not its introduction.\n- **D6 guard (2026-06-10, restoration D6-3):** the invariant is now **asserted**, not just noted. The pre-ready durable writes are **enumerated in one place** — `spt-daemon::PRE_READY_DURABLE_FILES` (`rollback_compat.rs`): `deadline-<key>.json` (D5, `DeadlineAnchor`), `applied-state.json` (D6-1, the two-phase `AppliedRecord`), and the generation-stamped `brain.ready` breadcrumb (D6-1b, `{pid, generation}`). A **tripwire unit test** pins each one's additive / N-1-readable contract (load-bearing field names present; an unknown extra field still deserializes), so a *non-additive* pre-ready change (renamed/removed field, or a `deny_unknown_fields`/non-tolerant shape) trips the test and forces the migration **behind ready-promotion** (or into an additive form). *…\n- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n- **Closed out (2026-06-11):** the readiness-gated auto-rollback shipped (v0.4.0) and the pre-ready durable-file registry + tripwire guard (D6-3) hold. v0.4.2 added a promotion **bytes-gate** that turns a wrong-bytes respawn into an auto-rollback rather than a false `applied` record — strengthening the rollback path the same release exercised across the fleet. No in-place schema migration has shipped, so the invariant remains **asserted, not yet exercised by a real migration** (correct — activate-don't-pre-fail); the tripwire is the wire it trips the day one is needed.\n<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\n\n### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_through` on the live-send) and then **discarded** by the waiting loop's `_ => continue`. Cursor advanced + content dropped = that chunk is gone for the downstream consumer and the broker will **not** re-send it (resume reads from the delivered cursor, ADR-0018 D4)…\n- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead code, activate-don't-pre-fail).\n- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.\n\n### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same constraint that moved session continuity to the broker in D4).\n- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The update-vs-crash decision is the D3 spawn-time `StartReason`. **One-shot** (alarm) deadlines persist their absolute `target-time` at creation and **never reset** on any restart (\"remind me at 3pm\" is a commitment) — the asymmetry vs the periodic crash-reset is the rul…\n- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired helper, the port builds the *scheduler*.\n- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).\n\n### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, and the daemon records `applied:N` while still running the previous version. New code does not run; the record is optimistically wrong (the enlyzeam-class record/reality divergence, now provable via `exe_hash`). **Windows** dodged it — `GetModuleFileName` returns…\n- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof the new bytes run). If either hash is absent the gate degrades to readiness-only (N-1-safe for pre-metadata releases / a missing breadcrumb) but emits `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.\n- **Origin:** ADR-0018 Q3 silently assumed `current_exe()` path-string semantics; surfaced by the v0.4.1 fleet-roll `exe_hash` bytes assert (agents `todlando` + `doyle` + `deployah`, 2026-06-11). ADR-0018 Q3 amended.\n<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\n\n---\n\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\n\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\n\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\n…\n\n…\n","startLine":200,"lineNumbers":[200,201,null,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,null,287,null,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,null,374,null,664]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195934454}}
{"type":"message","id":"c5714b70","parentId":"c4df40a4","timestamp":"2026-07-16T09:58:54.455Z","message":{"role":"toolResult","toolCallId":"call_cPGOTjCCikURUm8JygjNjkbR|fc_0698befac979932d016a58ab5d54b8819a840166c84d6de33a","toolName":"grep","content":[{"type":"text","text":"Skipped due to queued user message. Do not count this skipped result as completed work or verification. After the queued message is handled on the next step, retry the skipped tool if it is still needed."}],"details":{},"isError":true,"timestamp":1784195934455}}
{"type":"message","id":"b6731bc3","parentId":"c5714b70","timestamp":"2026-07-16T09:58:54.455Z","message":{"role":"toolResult","toolCallId":"call_VurTmSy9tPCYvzlsW53XKj8R|fc_0698befac979932d016a58ab5d55bc819a961abb30d5c5d108","toolName":"grep","content":[{"type":"text","text":"Skipped due to queued user message. Do not count this skipped result as completed work or verification. After the queued message is handled on the next step, retry the skipped tool if it is still needed."}],"details":{},"isError":true,"timestamp":1784195934455}}
{"type":"message","id":"af121e61","parentId":"b6731bc3","timestamp":"2026-07-16T09:58:54.455Z","message":{"role":"toolResult","toolCallId":"call_oe1tuDY6Jj0IsdiMU6lShS9J|fc_0698befac979932d016a58ab5d55c8819a87d1b2c816550798","toolName":"grep","content":[{"type":"text","text":"Skipped due to queued user message. Do not count this skipped result as completed work or verification. After the queued message is handled on the next step, retry the skipped tool if it is still needed."}],"details":{},"isError":true,"timestamp":1784195934455}}
{"type":"message","id":"efb4e439","parentId":"af121e61","timestamp":"2026-07-16T09:58:54.484Z","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":1784195901825,"steering":true}}
{"type":"message","id":"417dd417","parentId":"efb4e439","timestamp":"2026-07-16T09:59:47.475Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Refuting incomplete dispatcher hypothesis**\n\n**Analyzing race conditions in stream reattachment**\n\n**Identifying dispatcher restart reclaim failure**"},{"type":"thinking","thinking":"**Tracing stream claiming and worker failure behavior**\n\n**Analyzing request eviction from stream ring buffer**\n\n**Examining ring buffer cursor and classification gaps**"},{"type":"thinking","thinking":"**Analyzing session replay behavior**\n\n**Tracing session output direction issue**"},{"type":"toolCall","id":"call_bbqHdqdRoXArEJcKNhkwJ1eW|fc_0698befac979932d016a58ab8a0de0819a912fe2a0ab228d36","name":"irc","arguments":{"i":"Reporting confirmed lifecycle flaw","op":"send","to":"Main","message":"Refine/refute: run_brain’s session cursor-only resume does NOT itself restore rc, but first heartbeat spawn_net_consumers→new dispatch::run_dispatch_loop is intended to reclaim every broker-held peer stream and worker→serve_attach re-subscribes PTY. Strong exact failure: dispatcher claim is one-shot per brain (`claimed.insert(stream_id)` before spawn, never removed/retried on Unknown/Failed), and reclaim reclassifies by replaying opener from seq 0. Attach streams are ordinary 4096-chunk overwrite StreamLogs; after >4096 operator→target chunks the original AttachRecord::Request is gone, peek sees Input/Resize => Unknown, stream remains claimed forever. Older/busier 4/5 fail while newer/quieter retained-opener stream resumes. Any transient worker failure has same permanent-until-next-stream effect. Detach+rc creates fresh stream id/opener => works. Exact refs dispatch.rs 205-225, 283+; nethost.rs DEFAULT_STREAM_RING_CHUNKS 108, StreamLog cap/append 158-224; attach restart test only manually re-serves short stream from seq0.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11378,"output":1541,"cacheRead":160256,"cacheWrite":0,"totalTokens":173175,"reasoningTokens":1249,"cost":{"input":0.05689,"output":0.04623,"cacheRead":0.08012799999999999,"cacheWrite":0,"total":0.183248}},"stopReason":"toolUse","timestamp":1784195934498,"responseId":"resp_0698befac979932d016a58ab5ed5fc819abca4f93faed2c4b0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ab672ac0819a8b94746deb142ba9","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKt10jOe5_CJ-ra8TDLAeM-8ivkNWdRll1kQDgsR44hpvBnr0Yhl05MGxfFle94cz3d561wHAwZfDlq11_bU4QdBwJIBrBftzJIPtzSHv9USbEDDrveJQmKyekFr4wLVsRh13gENrpT2T6pkpOlNi82DYcQT4_H_e_WFuJgOCjPmaDgtHEDxbRcvZo6KrWJWWXAEee3tdfOkHungf1NFiGwjnZUDYzpAeuzHsfMbF1iOOIIAfgI1u4FNMDkAoB0VYzhE-JLeoVQqCXUSHSsjp54tM9TPrfFxaoDADo6_UA9HYHAD3E9hnrZtHxQMhIkoScSkwQytqte6du0HOF5zkzm7srL4fIAROkJCet9IOcx0Va-WjPiqZQBX9cDHGE7JeSKMWJMrFXXhR_0XLv2cJcfsewPj_CtTkOwQfqcWKjePTlPsmn3DyYYkfh4HxTDCIxanpkQz94LlhdH2KbKLkA5c_PbQusCJMelDg96riPInoRnksnUObHTrQzUb_-_WtZv38jvbG82O1YQT4cB5PE3LRr81115tJK8Z9J43Pp71vCVVso6dWGNNNxtELfG5bqUe2uFRHopOX6tV_N9GHuHEKdlTcZJCXMWwImHg9FuM0d-3e6go1zd0MhkxmvpSTm-okwX0LiBrOd0bek66os8B_AqzQGmR76oVchg1ZMwyQ1vRs6JWxlv5PFVxlSej6ne3LnsKwBBDq1ug3-zf5xM0AdKa9OK6cySvcRt35kGYD40FulUiplev3TjdEA0811wwj41XIm_LnXISY81RD2LQ5CjGHYf-ESZtb_WltC4Xsy6g5v4GciJV4v8Iu_lmcw4sOeDvqmHft5hjBZNIhQLgFUeJIWruzHPjACziQejlYkxiBWnRZ4GHVDwi_zS96GIQTWr8NMMhkeDHUfFVX9qR2eu9o0Lf2TsQb08cx44saLtKWoaOqlo2MuFDetb6vZA8ntQo2XNNSLa9VFYv55FfVN3NeYJNlzb08_hOfsF27P7QNrewtoW54yCFsChVRq5i02-8Xn7qR0bUxRs-MrhelWKVJrMr8JoTgmBIRypyqTnqTwX-L9L-nwM_VpNLcDhzVa6meGHu1QVTx-gJnOGbEwLXcxC77Pdqhkv3pPw9dL3WxbOnNWwjNcVBg5FMLAG3U3Aa3qzPleERvjrSrsZ5bpQ4fqGWv0vTJ_6gap7OCNX_6iuFUlVlF6ygNla9OEIND2ianxObopeV5faYJE0NHO0LFag2i3dXPckR-Jm7mj6gQyVyKX8J9ZM57cKAfw_UPFX_RRbvO9PoCJtX2KBUUGEW3vCwLEdGtidQew6F17xLE8Iyp7758olkerI5EFdkPIOE8WeXulv7hp5BtZCZiB-KtSQrLttmcQ4RrUqZWlKGJYCMohE2HXZ4fmr-ojIBDRd6ci7kclB_8bRkM7o6H1WiTBuQ7ylwrF5uWCe-9raatOW_y5wVCNAHYWcCzSYdcg-AhvcmKMmQnUJe8ty6xlYrkU3cXt3f4l0lRP4RPG6n3xi3H8twYBU8QShFrxYjh_VIVXdxV9bJX2QMVjUuAWmuvbxZtXw_67dsy1XdL9sr8SkvmDk88CHJxWpyxBxJTAWqK41oA4a2MFlZZBo-p8JR1pLN34m9DyH0KqZV8vLeVpiOjj1hvZ7bBSL7X4_gLwSn6YFoMJClsG3M7h-_en5Nsjh1XfVV3FI3SKbFUky4L9DbW8seGs_VpE8bQzcXRTNj8zgQLaeSyYn3cRWJ_k9Y5gvs4ef5tuH-g7-ZeEe_gFumV68V-qs5zmiyZkcFNekutb8lJR6_S70_RKMmwabelO6gKT8kPVyhE3perz8dlUT9snq4Su58l2i9OEj6USOAOEttw0u4VuD8WgvdmCXuHJSGjjNccKkt8y8KK8QE_W1MgNsRkzZ7Z9a8KFGZtfsw5qrG2GkOKQrkw1uedEeGTvwOOIv5J8v7Kv-dHNm84ggPhuzDGIdGjk25XE3VU-r1W8yAxIn6yOPWsjqhmCSgtOgWGYFfEoLaSMDH7214IHMxC-64AyhurLzhuF0Qbvijh3wy3hm0cNh2w0FsviTI4y62RbIX_blwVgfLL57zDUmFEM7T4NVAYPG4EXdrtiZqN4E0h5FCG5OdUad5HGpDrTm4dMdnEjAi3M5CgR6lanx3WSs-Ye10Qd4So6EQu-vY8SsFEX8vK8_oYbD9fFAIVEGmU9TyqVILdPkuVY-k-edQn0SAvJTxfjwTj3C5-f7TXebmh-RKRn-bvwxO-CZB7UpXTPfeuuYn0oVwWlo9yfhX66ctCBJR5E7rS6gKaoBZ2d5YjYGQFp_Md1igk5xwsbyjVM_rZngYsRoRwhNVqw2x72yH3EzKMRtWtTM140vDr-9OToVD3pj_ffWcDa4EF_e9QOsd1zyiQKmwMUpzs7XMwFvMLPTm4IMybSzdC-76ax84Xu7C9RwUWy6W23oP6wuBBYh2s81AC3nHz_dSyRlyoXlGjbbmkeO-d7eUH6rukwrew78mi8ODVnJYtOiIwkGM06Yuszke8BqNmsC-yj8Gd2CbAAm04c5pyUuDBB8l2e5husLaKZjcAA0fmf6R-4F5LgI6wDfchzXNsyEqQc29oM4q0D-THQz__5HEumdOogHg3Obl2Y4ioNJCXt_b_mhfMQLkaOmw1JsxpPHkedZJX4Q7k3djyO2MJTRd8hlgWcoEVIMhIDa5NQEf9tW6XLRMYSRN8fWdgQB5nuQrH_mKr0cd1zOY_KQg80BMMX17W5huCOATRnCYcKS9Mr5QsQbV76sg-dWo4IgwiHDRxKtnVc3Ljnk2eVcaIcm0N_PuBMOHB_s34RtbewJU8XkevAGaVYaBidcMUcOAeSw0Dcno8mglI9NttTznjiFRxTLbHGqmej7ringu87oHVO-Q1WBXK5aCM7Akufhl9FWgKNYsnqLwtC8yM8KkYYa8xD01vonmold1gQ37VarKDvHupvEt7leuti7rdmScVbIVs1QvU4wbsQEwEQb8MPVrllgs2nKmWap-0NqGqpi-lRKU3xdB-Wt_c5IgiCsZUHU1d5Ci_XEL1vXZKKFu7hTXI8G8-g_56UwNufeBwCo6Daqv2bEZPl59bF5Oc2aJGR4mGTAtAJI5ExyHT52CL9Ca7SfWrbUkV5ZX55Cx4t_OkauFnNqzoYpfnh1KGppaH1yqu2Hi5hDcvkxh7FsMfKcpZYQ0ogtfzucBux_qYaikpzWLpPpopokSvtoKBn5KAuSC447G7zWUGfHig-0pvBFgggPSN5vAzsVG5K_eLao6KXTpzT96E6U7Beyzd-u9l4Aytf7eHoWg51ad8JQF-qun2ivuJj5-mKw7AAH00iaSeEtX0F_oS94HIPg6JSUDw3eIU8VXj57zoDCuFMfU4zWL4fYGCcqbxs1pDrl8oRm_nx3tKA9djb4w0oJgeZW3fI8LPrDQYLXS_ZbvKcv2RznoPyYa40NXgF5a7pt_VYDNvXxp0RsI8g-gI59jSJYB9oQtcNyOTLs_7vPjXZKurFIbsBJUhtU_axP4hTMy7MwnGzIdFqxTuXxpTPkPzF8vwdtsFx6uLp-g0q6gG38n29HJYuVts6KyzX6DPwuPUbQ6Fac1PGR39vmBzhZpDIiFnLnXval04XRY52lAZHuHp1sxoo4SDtfG9uOWYRr0s9U4sboKIa9w-3YQZSL8ELui4ZuswIoX2Es6lVbhQOXuuecP9d8u55OGuWtDXimtGbv5E4-ZasSjD4Q5jZitE5-95Qsiy5YhdyU5sOr0WnKlhZOaTcJo2r0FlzaJFIFbXpf8RCECIz-eZWrEX4H6o_539079ew8aRKjbnleAl59JwD1Nth6IHpJ6h2hz1ftTCIElLJ8mfIEt3AIyy9ZOSB6PQmEMKO2hTWYZWEwG6AL3HviTWfAyKy27wpieIbfT7B9MxXek2R1u4swSJjqLI_5kcsndkUvSVtyamuPRwgjRI8lUdOqt3Z2NeBKqYMz1dOTO-Tp4Wm5Ea0L_t8ib25-mcDwCmbqnAPUQZNgiB5AvOgX1hEyNA-oru2aEaJbZz_dVlBTPge0nGO8noq8RnJQ0c_r1SGC7OLfSjQ0qeUSRaoAFqRTpfrtlB3d0SYHBONIdX4MCZL0ElWa9cPTkVKht_ZPWhq0ZwhyvNOijJpuFda_F_EBb_53Ajha80BFQZlnFGH2AxFC9lkX6ygFyQybe7CvxPb1kYIdS7Ds_Sn5drCAz7WSYmTFYBbi5FOwXls-44WLe3ttb45M7lf6U-ZG2_uIEv7hJpaxo3Ws24Hng1PGwVAFGDAI0Q2tQWzW0mGZSlg==","summary":[{"type":"summary_text","text":"**Refuting incomplete dispatcher hypothesis**"},{"type":"summary_text","text":"**Analyzing race conditions in stream reattachment**"},{"type":"summary_text","text":"**Identifying dispatcher restart reclaim failure**"}]},{"id":"rs_0698befac979932d016a58ab752dc4819aadf00bf62960e708","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuFIluvdSFMFcVVW89UbGZ9uA2v3e4dTFObgXoKbnm0qLAwLTjHFCnKNYVdDE3CIvPtjV1rSRgPjFdqULPdoX2avTFoy7lDvfOHmT0zxYVPNZbd4yMvsIxH_E1ngL0S4Z5yfbT027X00uWJZy5_z5RyHQO4GhjXqeeiRwFkUtK_PpfNnwazkuisOY-h6fCP8bpI8LmUfhA5vxG77Wi23uGHi1spJVL7QY3BFf8z9T4xuP5WS2GZwAXUmKuXMAW6ZIt65vE9WeKCtO7SWGJdX0MjGfyo5-Jm6ZDjKrPcEFwoPXmMkj0N8zCSN4GZD8rVOdSsEDc_4YiN8Kl_I-uQivYeYRZ2gg7QlJSXOz3h9d_Ai6btVunryFUgAmtSu5pMIncTG41_YujN3TmRvVIpRKtAgaBQyVr74rytmDltSBeaSfEQ_q0sboWteiaJLEt7zmOEgV2bW_yOMNc42TIdVRhJYPE2Z826asHUPT0TTte7CEbzi1HPj0G96QMnhdbQ6fEn2s5ihfcCdkXEvkVsr18OIn5KO3XyBffH55cxJtMIl4g5hr1mEQpyAhHf5-kQGXskPo2o-uqfsw5q0y6uQM8Kr9EbOJQw1_5R-YZ5k4kKLY-lYa06ZBAyjVqZ1Qb6v86vKigQm-sDualQlCEKVgT2At9LTQBk9WuWinB-MVK54UgHEcuAz0GEG5MEcnADm76p5uG6o-NTbpPczjx-y8uRm8Jiw78FA2k4tk3NkEwrmaWxgxm6achwydsN71oXqfj0TbyIUE4SVoAdP9k4ioDEwz6BhzTd0DdZHAzth7y8RWtI3cKNOBYEnRvLY-oS8wwQbygWllvhsGxGJyAUTF8M21KX_0I8x4EfBxliTpmUwjk3CMTJ-r-ZtCB0PGV1MkR3e0Cf8DZOns_Gta0LLFedgMzyypyMaEdLt3Nhw3-yNTitbpDI8ul4OGGkUQsLcmYLWpZ4_l9EJczNDWSG-dbk64ZtQQQ-sflCz5vWKHacKF49WNycTRFXo3eqvD2qWBZUmoeTStsHqXgTKvZ3e0sBBC3LitCinveKN9KEnox9SMna9c0rvTEakFfKZjpOszhnpJAFKjQl7OYlhaD6lx_dLPSFlO282xWcSdVy6GnkwDNwLIdTIWsOJKCmloEsbpqWSZwjP3uQHE7ly_0qGIYy73cf1egS8LFQsowxAifdiOa6JSFyjFMDiXYngsNplsvrumCcAslUhHkBpio4atETHtv-9RiMOjCSnjtN9Qt0mq0ej5T3b8zGukPTSfCDveJlWlnc27xLV0uvUGateplDmNR2t8IYEubg9oqzU0zLFFXrnhlcIbbvHiHbbTnT5W30yjF7cZr608p-YEr6UuZr9anj9o00Wx2EOlJTeTj7WDq4Kp0Gw5cuE2tSKVybQySxKPX1tpyS5kU2aDJJnVOs_mQReDgCBVqliPmadB8D02BXjhe_6ryMoyjDcIC4ewFZb3rlAN69Ysb-RFkPA6Cvn6to6vMSuzMp9wLmUXGFAq2HLNl2OH9mB7iCJ_y39AIAKwx4C9cS0U7_qZg3HWEOWRFiAwOKxDPda2xNc8RtPq2tFdFcVRHaf5W30LAtKZkxaa533qzSgPLOMMjGBC3egc5h666mYaNFzOia8YcOa8LOPc0t_JLO-OrNe4HpUXpPyXXjef2_oXKz7pWOUkz2jOZvpgp9VDrKs-uDmAkDsGc43yJNw1xVhHtyVBGAyaQ0rCglKn5EeiocgUeqBfQ2Y_gSzyTvigXbA_tSRzaYRAXcOHb6P71T3BWr0vvltaDDs8OoemYoBVBq1ztG0fEa_1DmVi2opjt93FdQhKO40EhEloXH7yRL3biCtY_qKWb-sSGLniiY6KiYSTPDcmecTx1LLl9sgCdUt1bTriaDzRMLM3WPTfjmyjf0lM-7dr38Ff8578MdjQNpefYSR6zH-fDnP6isFPhRliW8BMt49m4QEQWJMkteJNSl_Iwpg7xB41Gfe378GXYhDozqi0WbAmYVlLhw2M22AlPx9S5MEobcSSodvK5lm-etnO5741lpS3SieG71Fr7fgyGlYdjixUaHXOVVJaxphefM9QuslPUOOB2uoQ0JHiTiXgD4lLspSUQ5daYMFMGkZ8nWawVkFLFgU5R0oUQHNurPh1EqhA1Tehxm6S46VlsxPeS7T8NWM1mZEyaxxremRYf6t-wZ8vbHMP2Hv9cDKeluRXP28NonrIn_9kA3ORR92j9DflhySKKGpdy3yDLq4mob5043rR-d1XXcWpbR8tQv5bRXzy6LboO3nOeDsvriH6Rk0IQyZBfZ2UHfxDRaRf0uhx6gPB88w40T5sVla52_DyBKhvfK-KR5kpExGvtIAvIG3fSCwbMRfzC18OCbIYnYP__ybbK_pcme8JGOqDUnavxJFezGOz8xMHxsnXu9Wse_b9NbVs-EIDxg_TXNtuymrEseIiRwly4z-JaQV7XRkV8H-886cGyfy5I3vNKfrQxIkHO6c-06pj_H8RG5aFu-qcSWdRsDFLYOGaQITeNIBarS_AW6-DzJxXaYHlk-l0G5yowv1EnymtXgATTf61QQBjpXV4m0bxgAxxy8ksWX7KHIxvEq6erErbeLPqVzYnh2ME1yxeeBiPGSEQGty-0rPDQAsqjBAAQ0tM6Tuu-HaKcWzDnQ1-1-S_taC5jtz5-i-FqyLAKXmWkrhS4SRGvJ8uCidw7A-TdtpQr3AD0LR5fCaUNShGhQMHLh3PbBH5poL0CMd7uqi-atV4P5-mB-3ac7Ehd88aVt80cGFLdSuqeBZmcFHqYhgDzrNdeqZOqXePgtxWtlOvTW4Uzax5pVnL_aaEaQueCIFc8boCWcAQH1mjzke8dhIW9vJLsZhxWaFUFwCSNkzWJdIij42lq8NAFeValF4210P0MPLByL5xKKjU85xs4qcdPTjx66sSTfhYTu65MIUov1muiX8zG_m6si2vC3PD8sT4-RjzFAtxPI1qr8SKuD3d5frWWJaQMH2uG8riwElh9CkjhoLAhpzzpjKkq86GdGVZrUAo3rhGXr0frR8o9mfL8VpYCzdro5XAytwbJRSEpNUc7Tp6kziVSp7SrWHHeAiscqHOQUBArVN5fVYmIdSdiDd-nrqQDx1uzf4gt0B9e_2xjjbH0GyeeZYRzMVr5JcYADXf77KOWL7MT48khi5FWT4xK6QNlxtSBLgvk0u2c0n3oAf-A02kWVVRb1O3yEk13H9baHe5TWEeSIx6noRrjycQcKl9UYzVLOikWDRc1YfC94ko2-I3ULiF8I0-xXUlDlzKOjAqbmtcXMzr_doKQmjLUw5yICrZ9EV_uLRu---4qiYEiVU5TBSEESuBxLkRlYbF2KDNT97zLAAekGUi7PpAxz52V1-35lTD8lbjM6uOcDfrnC9h9wltyfPxlS1yb0qmz32zojGU4NqFpLr8rLqRetUxG2mApSFYLSW7RqQDLi5lA0xQL3FWCwhPa5kz_UpoCLNX0FU_E6e86I_8r2hVURKW0AU7sjSpSK2wQq-JYsF6tZS2Tl54ya77PTucFM58XgkianPh7H3ArRdxnOr3aXWfx3o613jvRAIimFlH6X5BzDBG2iMkK1WTmEey3sOmH_IjBWCCJkbXda9mXTx4z0QwHp7FHrRyHCIuJ2E4dO9nquEOyraCCUGxYBpO-_inpLlX3K34dCEpoNLCGZTz3WC8LjYV0epPQxQ30jQdHAqG-8nbF1NmEmLF2HdQ_clUstiwN5WzVitW4BU_POiHBI2YlZCR_He3Y-ioY4ec00x5QSndFv_ghTeT3b5fkyRM_FEy4-OMke0Frj1C_3RaD2lZJTZiAdZVrWAAlEO0BgCFKL7_XRH9bfD23lT3o8D23e7315FM7JhqlQCYn-Jk6C7TzoCkiyU2GuDghHdASIhI5TnSydtI7lHi7TsoLEKnZL-9MiFieVlAshdO_EB58267LCejMMrZLQhMkEQTrxsuoq7w0XuFFMAc6UQWBW5Bewyh1PYG1OQRgoZ6MrFXLTMTBj49TTpE2QJ62UI5P8ARA8xJhAcdEachW7ALjdhJMIny-2lg_21adEOM98HDgDyLwYHdiTFoYsYPM-tvc3V0cZ80ezAjuR-dCgGDf-Ib4dZv8WKJfEfsztoI0zB-hTbhHTWfXEnUmdpSrbiMYC_ksE8Yva0_zLhrDvunIVqgIHC5ZxzOmAnypDs41lIhqg-HjBd6ho","summary":[{"type":"summary_text","text":"**Tracing stream claiming and worker failure behavior**"},{"type":"summary_text","text":"**Analyzing request eviction from stream ring buffer**"},{"type":"summary_text","text":"**Examining ring buffer cursor and classification gaps**"}]},{"id":"rs_0698befac979932d016a58ab855e14819abf0219c8d18485e9","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuKC6MtYka-gmXOChbPPmDEwK8kROoTZ89CuMCA_A_IHsoHv-qoenzSnw6LXMgP7fOG_uveZUpKyCxkJbGAxyD1a7iI_OnZfRBFLDLa12ln6vt8BbHUOmcaRg9P9PiGh83R8u1AQL42naOfBqn4W8H7JIDFnNuA87mCXj2K8h7T_rHWGRsEsSrfSg9UNHTv2Px4B1qgzLzwE632dtEnhJuTE991HipKzFx_F0HLPc_z8dTaUnY6Ljma8-KI29f8_iDv8bzhTs_RyFr_8i2FPv8INr7Cd5nSDmT5SE_n0C1l7t1yV05wLQt4VE2bAm3WSuOPehjQumLrYRA9PJf2RBPIregCy_EbPzNoxQXA0KVwx0SReVoL2WuUd86spJPGarNh9-YV-eO2NoYbg6B21Ya1q1lu61-Owfp2RWa2MxKDEbaGbJ6zWgU4ITK49Co0EPcwmZ8hE-GPPxOSyUOeuwC13V55x6Ap3WmhVUgFaE6cQna9ixjUi5-aEH4cK5EfoJqPQjVLcSoJSBWP87VUwoovGng_FouQ-H0LSaAWSwmufxLbTLQKBpMvHmpEQ_E1QIfdNJsGsCRDrqs2VCC7NpWEpJ8yrValZ7FPBtAFSF_bCIvWYWvNfqofQ5rfkETSZsfyEHM4U_4Mt6wAD0MZq1foXPgxrZ4YPboFYWvtrGJaFBdepVQs1W7u53YNGAt-Rlu69cY3TKuasc05aLhGHftm247gxXFBzZiHRjkwFGcoIqUFNcWIFhalkIitjv7HUVqszjFJCrOV7wb8BTHLoQZsMjtQdySwICFL_OgKf-cgxxutNdjVDxcU5cXvkHNMNzslvTuG5qHZmVymB39ZwKfZQrkm6hC6FGKEJJmVCvzDspmBVqlIzQCMqcUIA8D3gsdfGZO27CMI2j5pxbrsO5w2sefT8A9FeuPQCopw1XLzQ3Fds96FlkPR_sce7aTAWuwkjr5X3wNDkBszQaRRSiQEYmG3p_odp5ZTWIZPo9zA7zLzTh9uy9LC-oMYrwYdEUdA9kOZS67Zwsw-f3Voccw6aElrZuibTePUzFQd7cPhE7ks4DfYXYn6AJE-NKsdw-YqihqAFG60fzYKwRaDxe2_5V9d9ogc74FFg2VVnMos7EPsuAejyjH5OIWZWOk4efFEcQTIWCLqbnfkxty7SNUljyVuAc4VVCLoJ09S2w6S76zWL9K1TlD-m79zArpfI4i93gPincN0MKLc--35tLIcPD7t_Xyo2Ffe19DCF_yefbXRMDE4f77SURyfSXiJUMrYK-9Nlol0wYYiAwggEHvTf1NJBmN3K_GtYZRggJhehtJYgmH8sohrLLG3odAw6d0lZhTPNI6V7mHNrwYHNNDFFtAdCeVVmnCaGI0d9bptdhcgTzp8A7ZArqHUEzt-GyBVIrKhlTd8Ww9rcbTknV-2MC04IK_yDH4ieNCSyGvBZtPj4L_sAvvw4RPmSLx0IwxEeL_FpRCWa8-ypPUCxogoxlhK0v0DyswoL-Q7Hiea8LSIV1raiJ7YMOS11RMl-N9cJqCZ8okcCWIAZRzigH54uVTXz5uKV_hPNOsi2h9NX1Rf0mulXEu73IYSxJUTt34h_igKpiP62vd6sL1Mz3njn-JHGUpCgDfwHiAo6oDCPRKMcxpVOW6mhxUsO972mn8DlTCMU3XmYf_XZ2QipL4QQgV4sSXcw-rsC0lm6tITZJ-j1J1iDst0ku5jG169Qm0hdfxs1rZ_0uyG7EK-EWFuTygVKJzVTp17ciZSy2HFHznxjfam5dosq9AJGVrp3uV8r3LDkNISPiUKR548X-KyS-28rq4c_oMUZxCLYF9eJCkioFu-QVW-yN4_F2DaOgQVfkvCQxbFjf0RI1mc7Tj_rbNYsvGFzsOa1GVdQ7QmzcYQ44KHf8KlXI99gHt-LtZyV23A8IBureJ3eXOzyXnJAKn4CZ-21EynpT-D6W6anks4rULQtk5KDrmxKzB05sFTgT_B5BHBM_-mA1pyfJ0lnLXoo376CCzeeOhog-KC-7iT6mAUABj2MPjYknLmgJEXwQJhfUuTjYOYzP_96cGqrWl-H9IKUa_1himAZpMs1fDtiJ97NPJpgdOpfTywvJjapGqmyB0-wXjMJQFzGsGI6BxZMr-Yb0GddyQ-ligpV3FONylnLhmYwKtW9WBtG2-4LiVBrw7_wTAM4r_6Ia8Lyi02Ti0SU7iJcOg9t70s0ohNiz5hLy6Cr1x_N7HacyWdPnuAjxvDQVxF9vHvB8gDrZDvm4efRMHXvc4v3eRUe2XJj7BLgnYSFBdI9qqwFdK0qiio6zir9TeS41vJZhznfpN-CJ8AjM04wSKfdHqYdVk=","summary":[{"type":"summary_text","text":"**Analyzing session replay behavior**"},{"type":"summary_text","text":"**Tracing session output direction issue**"}]},{"id":"fc_0698befac979932d016a58ab8a0de0819a912fe2a0ab228d36","type":"function_call","status":"completed","arguments":"{\"i\":\"Reporting confirmed lifecycle flaw\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Refine/refute: run_brain’s session cursor-only resume does NOT itself restore rc, but first heartbeat spawn_net_consumers→new dispatch::run_dispatch_loop is intended to reclaim every broker-held peer stream and worker→serve_attach re-subscribes PTY. Strong exact failure: dispatcher claim is one-shot per brain (`claimed.insert(stream_id)` before spawn, never removed/retried on Unknown/Failed), and reclaim reclassifies by replaying opener from seq 0. Attach streams are ordinary 4096-chunk overwrite StreamLogs; after >4096 operator→target chunks the original AttachRecord::Request is gone, peek sees Input/Resize => Unknown, stream remains claimed forever. Older/busier 4/5 fail while newer/quieter retained-opener stream resumes. Any transient worker failure has same permanent-until-next-stream effect. Detach+rc creates fresh stream id/opener => works. Exact refs dispatch.rs 205-225, 283+; nethost.rs DEFAULT_STREAM_RING_CHUNKS 108, StreamLog cap/append 158-224; attach restart test only manually re-serves short stream from seq0.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_bbqHdqdRoXArEJcKNhkwJ1eW","name":"irc"}]},"duration":52976.050300000235,"ttft":8743.926199999638,"contextSnapshot":{"promptTokens":171634,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bbqHdqdRoXArEJcKNhkwJ1eW|fc_0698befac979932d016a58ab8a0de0819a912fe2a0ab228d36","toolName":"irc","startedAt":"2026-07-16T09:59:47.476Z","intent":"Reporting confirmed lifecycle flaw"},"id":"35be71c5","parentId":"417dd417","timestamp":"2026-07-16T09:59:47.476Z"}
{"type":"message","id":"bec0371e","parentId":"35be71c5","timestamp":"2026-07-16T09:59:47.476Z","message":{"role":"toolResult","toolCallId":"call_bbqHdqdRoXArEJcKNhkwJ1eW|fc_0698befac979932d016a58ab8a0de0819a912fe2a0ab228d36","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"RefreshLifecycle","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784195987476}}
{"type":"message","id":"f31d4340","parentId":"bec0371e","timestamp":"2026-07-16T10:00:01.418Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming no inherited PTY handles**"},{"type":"toolCall","id":"call_Z4LKGoTDSNdbFBoBYcZOCLtA|fc_0698befac979932d016a58aba10880819a8edf9b3215d23833","name":"grep","arguments":{"i":"Finding descendant process spawns","pattern":"Command::new|spawn_detached|detached_no_inherit|run_bounded_command|ManifestRuntime|launch_harness","path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/shellwake.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_kGKGFjClJ6vDXAXGvhsh6IZP|fc_0698befac979932d016a58aba1089c819aa6bf7c19037694ae","name":"grep","arguments":{"i":"Finding no inherit primitive callers","pattern":"detached_no_inherit|bInheritHandles|inherit_handles|handle inheritance|NO_INHERIT","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_YkxopEr67opVYXBj9LapnRRB|fc_0698befac979932d016a58aba108a4819a8221084e2df70698","name":"grep","arguments":{"i":"Finding psyche process launch","pattern":"spawn_psyche|psyche.*spawn|launch_.*psyche|command_for\\(|\\.spawn\\(\\)","path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_bYRLa42B0iGrAnsGOvzrwnAg|fc_0698befac979932d016a58aba108b4819a95caebddf6af5843","name":"read","arguments":{"i":"Reading stream ring overwrite exact logic","path":"crates/spt-daemon/src/nethost.rs","selector":"150-295,1490-1535"}},{"type":"toolCall","id":"call_aJmDCYTQut7KH2Ww5bSyNBFo|fc_0698befac979932d016a58aba108bc819aa517372b30cefedb","name":"read","arguments":{"i":"Reading brain stream cursor semantics","path":"crates/spt-daemon/src/brain.rs","selector":"790-850,1515-1555"}},{"type":"toolCall","id":"call_emxzNk3zyIoEN2mZkBMo1mN2|fc_0698befac979932d016a58aba108c4819a9345ea7de02d775a","name":"grep","arguments":{"i":"Finding dispatcher retry tests","pattern":"claimed|Unknown|DispatchOutcome::Failed|worker.*retry|ring.*roll|4096|first.*evict|classif.*restart","path":"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/tests/attach.rs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3724,"output":583,"cacheRead":169472,"cacheWrite":0,"totalTokens":173779,"reasoningTokens":110,"cost":{"input":0.01862,"output":0.017490000000000002,"cacheRead":0.08473599999999999,"cacheWrite":0,"total":0.120846}},"stopReason":"toolUse","timestamp":1784195987486,"responseId":"resp_0698befac979932d016a58ab93d9c0819ab94bfb406fe36b41","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ab966bcc819aabab346e3439cbcb","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuhy96SnsKvy5PZqxI-HfnIAfr7S7-EadtanvWyJn_NFUjn9zC6EIEMbK5ZUi5WNe0d0fCwgzu2ApHj5r-BO0hkAtN_Q3z_94XYYDlVW117CTs-YDT0ziafgNJsSOLHSJhg6na3S6vMPTsEvlxqqrlj9w5TlX9pG1smAvqWWzWAd2wWC7Gh5g8B1v9d90wXZH1mxdSCfmaiqtY1Ph0lGs8xPReAG6U4MbJB0tdaGF7hA2bZGcOsj___uwgElhqgCHZI4LRcvRDogps_jhY_E0G4SL8U5sefApRE4VpK6B_SQIO38CHD_r6fPC7UHnRB5rKJVpI9mxlwyjAEPJHtoS2IApSXU7AZ20BlIcTQtqAGo5ZqOJ4IarDNOoHapIt48l61QAJL6YZH2KuvqsEVNS_1ptcTRimiJaZpqKeqkSTulxG1BiNCd7LgGn8xDyBNoa5Bton0FPHQwaSVRPye0oTDzWdQgDDtCBl__HUBfi6ZrayVLs9P8KHW5zKTkHUDkJtn3JWudWm8wazPvP75CtCX5ws0EvEFftaB4Yt75TM5AqTxE58PGCi89FhK8sx2Qblza8MTjRZy5Ca8F9pQUd0zTEDBD6xgni4d9lP0KbnVrH6Z3Cs2fyaJynitvYrFJg0TR11Py0P7Viw6PPCLLnsadlDtMnhcBj3yr2eqTuch1MG0e0DCmYqJModY4yQh-O_kQ1sdzia0wlGWCDWH1k4-P9mVaqPN9IbggqH4EzEvdFKmJP1Ym3dnNV6cAHzFX3rd-El8IfrG8eAJNij2x8GoqlYyqAA25MSoFTsxoA6mTlprsLj3n1zQRZI0AuhZCDhMXfGQ0QlBIbh-YxB7hbwr6myA8xkhZa9LR_3CuWn0aeT9UiYbOuzwMrCtkyBgx2uzuETaMdNSf7gtOYCPQMYEKJjMX7YBbkaSoD4T_pytgB7wv_e7LUJrypVmO8526ZQcnLDrKcWlWxLuP49ifFKnFPeEmjg5tJE_mxDq26d2R0b3LNhPT1PGUBOhYM9lIPpmfDeRAW-T730B1RMB2D5Hc9glDaRiG_tQj_KbE6cGi7_g9c5Tv18fo0aEZIdJ7hCJ6MmbZlYtl2k98UgLnVoXUzuJHfFy6viB_cf0tILKAVkXgvdKGwFQwRdfrI8BIJVL2znpRch0pzs1xwxe1jMWtQKUCngMw_M4YUnbaOTxoFWZwDiV4FpYfEvTtqot9Klh2NNo9DAXF1MRxHD4nmw8Y5o5ZyxIy5ClbkNCBzE63lehufBSm3zK8HrImY6CoPBqUfTspO0UnaYeGgJEBZ8W8X_dYPo5Gf2lVzHU1oRz41abG7CUBWfOA3R9T-XfvBNRipQFsHRnSeFwXz20fmua05hcJCGGnhs13QyxosfKzmrh_D-y_F5AMQxpKK4eBac6lroQ8WcYyNkUgZDEHjvqLdRAH8UzwDE03hEsCYtNfUVKVoolspKHQ8lDfa93-y9gBzyF7jAdsEGN8zwKEO6aQ9775c_Q4imNQY53ukLpeLrtRFbSkimzlWJlc1GDOdSMo2oV89FOVS8fB6mO0YHYCPPqUel9df1rMp7p79kxrkNsnaK3KnAAQu8cbrcJGeJ1DI1iJwa7z0Wku9Rv4ii2Ng==","summary":[{"type":"summary_text","text":"**Confirming no inherited PTY handles**"}]},{"id":"fc_0698befac979932d016a58aba10880819a8edf9b3215d23833","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding descendant process spawns\",\"pattern\":\"Command::new|spawn_detached|detached_no_inherit|run_bounded_command|ManifestRuntime|launch_harness\",\"path\":\"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/shellwake.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Z4LKGoTDSNdbFBoBYcZOCLtA","name":"grep"},{"id":"fc_0698befac979932d016a58aba1089c819aa6bf7c19037694ae","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding no inherit primitive callers\",\"pattern\":\"detached_no_inherit|bInheritHandles|inherit_handles|handle inheritance|NO_INHERIT\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_kGKGFjClJ6vDXAXGvhsh6IZP","name":"grep"},{"id":"fc_0698befac979932d016a58aba108a4819a8221084e2df70698","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding psyche process launch\",\"pattern\":\"spawn_psyche|psyche.*spawn|launch_.*psyche|command_for\\\\(|\\\\.spawn\\\\(\\\\)\",\"path\":\"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_YkxopEr67opVYXBj9LapnRRB","name":"grep"},{"id":"fc_0698befac979932d016a58aba108b4819a95caebddf6af5843","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream ring overwrite exact logic\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"150-295,1490-1535\"}","call_id":"call_bYRLa42B0iGrAnsGOvzrwnAg","name":"read"},{"id":"fc_0698befac979932d016a58aba108bc819aa517372b30cefedb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain stream cursor semantics\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"790-850,1515-1555\"}","call_id":"call_aJmDCYTQut7KH2Ww5bSyNBFo","name":"read"},{"id":"fc_0698befac979932d016a58aba108c4819a9345ea7de02d775a","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatcher retry tests\",\"pattern\":\"claimed|Unknown|DispatchOutcome::Failed|worker.*retry|ring.*roll|4096|first.*evict|classif.*restart\",\"path\":\"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/tests/attach.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_emxzNk3zyIoEN2mZkBMo1mN2","name":"grep"}]},"duration":13916.390100000426,"ttft":2913.099200000055,"contextSnapshot":{"promptTokens":173196,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Z4LKGoTDSNdbFBoBYcZOCLtA|fc_0698befac979932d016a58aba10880819a8edf9b3215d23833","toolName":"grep","startedAt":"2026-07-16T10:00:01.418Z","args":{"path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/shellwake.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src"},"intent":"Finding descendant process spawns"},"id":"669e08de","parentId":"f31d4340","timestamp":"2026-07-16T10:00:01.418Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kGKGFjClJ6vDXAXGvhsh6IZP|fc_0698befac979932d016a58aba1089c819aa6bf7c19037694ae","toolName":"grep","startedAt":"2026-07-16T10:00:01.418Z","args":{"path":"crates"},"intent":"Finding no inherit primitive callers"},"id":"81a151ab","parentId":"669e08de","timestamp":"2026-07-16T10:00:01.418Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YkxopEr67opVYXBj9LapnRRB|fc_0698befac979932d016a58aba108a4819a8221084e2df70698","toolName":"grep","startedAt":"2026-07-16T10:00:01.418Z","args":{"path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-runtime/src"},"intent":"Finding psyche process launch"},"id":"85dbe97e","parentId":"81a151ab","timestamp":"2026-07-16T10:00:01.418Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bYRLa42B0iGrAnsGOvzrwnAg|fc_0698befac979932d016a58aba108b4819a95caebddf6af5843","toolName":"read","startedAt":"2026-07-16T10:00:01.418Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream ring overwrite exact logic"},"id":"baff0249","parentId":"85dbe97e","timestamp":"2026-07-16T10:00:01.419Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_aJmDCYTQut7KH2Ww5bSyNBFo|fc_0698befac979932d016a58aba108bc819aa517372b30cefedb","toolName":"read","startedAt":"2026-07-16T10:00:01.419Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading brain stream cursor semantics"},"id":"51d938eb","parentId":"baff0249","timestamp":"2026-07-16T10:00:01.419Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_emxzNk3zyIoEN2mZkBMo1mN2|fc_0698befac979932d016a58aba108c4819a9345ea7de02d775a","toolName":"grep","startedAt":"2026-07-16T10:00:01.419Z","args":{"path":"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/tests/attach.rs"},"intent":"Finding dispatcher retry tests"},"id":"68f36cc5","parentId":"51d938eb","timestamp":"2026-07-16T10:00:01.419Z"}
{"type":"message","id":"9cb7c4b5","parentId":"68f36cc5","timestamp":"2026-07-16T10:00:01.439Z","message":{"role":"toolResult","toolCallId":"call_aJmDCYTQut7KH2Ww5bSyNBFo|fc_0698befac979932d016a58aba108bc819aa517372b30cefedb","toolName":"read","content":[{"type":"text","text":"                KIND_NET_STREAMS_REPLY => {\n…\n                    return Ok(BrokerEvent::NetStreams(ev));\n                }\n                KIND_SESSIONS_REPLY => {\n                    let ev: SessionsReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Sessions(ev));\n                }\n                KIND_PAIR_JOINED => {\n                    let ev: PairJoinReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::PairJoined(ev));\n                }\n                KIND_MET_MEMBER => {\n                    let ev: MetMember = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::MetMember(ev));\n                }\n                KIND_NET_STREAM_DATA => {\n                    let ev: NetStreamData = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // Same exactly-once discipline as PTY output, per stream:\n                    // accept the contiguous next seq, drop a replay duplicate,\n                    // reject a forward gap (a lost chunk must not pass silently).\n                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n                    if ev.seq < *cursor {\n                        continue; // replay duplicate — dedup\n                    }\n                    if ev.seq > *cursor {\n                        return Err(io::Error::new(\n                            io::ErrorKind::InvalidData,\n                            format!(\n                                \"stream {} gap: got seq {} want {}\",\n                                ev.stream_id, ev.seq, *cursor\n                            ),\n                        ));\n                    }\n                    *cursor = ev.seq + 1;\n                    let bytes = decode_bytes(&ev.data_b64)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStreamData {\n                        stream_id: ev.stream_id,\n                        seq: ev.seq,\n                        bytes,\n                    });\n                }\n                KIND_NET_STREAM_EOF => {\n                    let ev: NetStreamEof = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStreamEof {\n                        stream_id: ev.stream_id,\n                        next_seq: ev.next_seq,\n                    });\n                }\n                KIND_NET_PRESENCE_EVENT => {\n                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // Same exactly-once discipline as stream reads (D4c):\n                    // accept the contiguous next seq, drop a replay duplicate,\n                    // reject a forward gap (a lost liveness event must not\n                    // pass silently — a phantom \"still connected\" peer would).\n                    // [impl->REQ-EP-4]\n…\n                }\n…\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n    /// discipline on a QUIC stream, D4b).\n    // [impl->REQ-DAEMON-2]\n    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n        self.net_cursors.insert(stream_id, from_seq);\n        self.send(\n            KIND_NET_STREAM_SUBSCRIBE,\n            serde_json::to_value(NetStreamSubscribeReq {\n                stream_id,\n                from_seq,\n            })\n            .expect(\"NetStreamSubscribeReq serializes\"),\n        )\n    }\n\n    /// The next stream-read sequence still needed for `stream_id` (the durable\n    /// state a handed-off brain carries to resubscribe gaplessly).\n    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n        self.net_cursors.get(&stream_id).copied()\n    }\n\n    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n    /// events produced while no brain was attached replay from the cursor.\n    // [impl->REQ-EP-4]\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n        self.presence_cursor = Some(from_seq);\n        self.send(\n…\n        )\n    }"}],"details":{"displayContent":{"text":"                KIND_NET_STREAMS_REPLY => {\n…\n                    return Ok(BrokerEvent::NetStreams(ev));\n                }\n                KIND_SESSIONS_REPLY => {\n                    let ev: SessionsReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Sessions(ev));\n                }\n                KIND_PAIR_JOINED => {\n                    let ev: PairJoinReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::PairJoined(ev));\n                }\n                KIND_MET_MEMBER => {\n                    let ev: MetMember = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::MetMember(ev));\n                }\n                KIND_NET_STREAM_DATA => {\n                    let ev: NetStreamData = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // Same exactly-once discipline as PTY output, per stream:\n                    // accept the contiguous next seq, drop a replay duplicate,\n                    // reject a forward gap (a lost chunk must not pass silently).\n                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n                    if ev.seq < *cursor {\n                        continue; // replay duplicate — dedup\n                    }\n                    if ev.seq > *cursor {\n                        return Err(io::Error::new(\n                            io::ErrorKind::InvalidData,\n                            format!(\n                                \"stream {} gap: got seq {} want {}\",\n                                ev.stream_id, ev.seq, *cursor\n                            ),\n                        ));\n                    }\n                    *cursor = ev.seq + 1;\n                    let bytes = decode_bytes(&ev.data_b64)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStreamData {\n                        stream_id: ev.stream_id,\n                        seq: ev.seq,\n                        bytes,\n                    });\n                }\n                KIND_NET_STREAM_EOF => {\n                    let ev: NetStreamEof = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStreamEof {\n                        stream_id: ev.stream_id,\n                        next_seq: ev.next_seq,\n                    });\n                }\n                KIND_NET_PRESENCE_EVENT => {\n                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // Same exactly-once discipline as stream reads (D4c):\n                    // accept the contiguous next seq, drop a replay duplicate,\n                    // reject a forward gap (a lost liveness event must not\n                    // pass silently — a phantom \"still connected\" peer would).\n                    // [impl->REQ-EP-4]\n…\n                }\n…\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n    /// discipline on a QUIC stream, D4b).\n    // [impl->REQ-DAEMON-2]\n    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n        self.net_cursors.insert(stream_id, from_seq);\n        self.send(\n            KIND_NET_STREAM_SUBSCRIBE,\n            serde_json::to_value(NetStreamSubscribeReq {\n                stream_id,\n                from_seq,\n            })\n            .expect(\"NetStreamSubscribeReq serializes\"),\n        )\n    }\n\n    /// The next stream-read sequence still needed for `stream_id` (the durable\n    /// state a handed-off brain carries to resubscribe gaplessly).\n    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n        self.net_cursors.get(&stream_id).copied()\n    }\n\n    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n    /// events produced while no brain was attached replay from the cursor.\n    // [impl->REQ-EP-4]\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n        self.presence_cursor = Some(from_seq);\n        self.send(\n…\n        )\n    }","startLine":787,"lineNumbers":[787,null,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,null,863,null,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,null,1559,1560]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784196001439}}
{"type":"message","id":"348a3697","parentId":"9cb7c4b5","timestamp":"2026-07-16T10:00:01.443Z","message":{"role":"toolResult","toolCallId":"call_kGKGFjClJ6vDXAXGvhsh6IZP|fc_0698befac979932d016a58aba1089c819aa6bf7c19037694ae","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n## Cargo.toml\n 55|# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n*56|# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n 57|[target.'cfg(unix)'.dependencies]\n 58|libc = \"0.2\"\n 59|\n\n## src/\n### daemon.rs\n 618|/// harness hook reading the command's output). On Windows `CreateProcess` runs\n*619|/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n 620|/// pipe's write end and hold it open forever — the capturing reader never sees\n 621|/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n*622|/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n 623|/// (the first guard shipped here) still leaked a grandparent capture's pipe\n 624|/// sitting in our handle table as a stray. So detached long-lived children are\n*625|/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n 626|/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n 627|/// `CLOEXEC`) and keeps the plain `Command` path.\n 628|///\n...\n 632|/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n*633|/// no handle inheritance by construction, so 5.6 holds on that path too),\n 634|/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n 635|/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n 636|/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n...\n 729|/// SILENT_BREAKAWAY_OK) or there is no job; a job that FORBIDS breakaway fails\n*730|/// `CreateProcessW` with ERROR_ACCESS_DENIED. Used by [`detached_no_inherit`]'s\n 731|/// fallback rung and, for the daemon, the [`launch_daemon_job_neutral`] ladder.\n 732|#[cfg(windows)]\n 733|const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;\n...\n 738|/// handles, with `extra_flags` OR'd onto the base detached flags. The single\n*739|/// FFI primitive shared by [`detached_no_inherit`] and the daemon's job-neutral\n 740|/// launcher ladder ([`launch_daemon_job_neutral`]).\n 741|///\n*742|/// **`bInheritHandles = FALSE`** is the KH 5.6 guard in its complete form: no\n 743|/// handle (std or stray grandparent pipe) flows into the immortal child, so no\n 744|/// capturing caller anywhere up the process chain can be wedged by it.\n 745|/// `DETACHED_PROCESS` ⇒ no console, so the child's std handles are simply absent\n...\n 791|            thread_attrs: *mut core::ffi::c_void,\n*792|            inherit_handles: i32,\n 793|            flags: u32,\n 794|            env: *mut core::ffi::c_void,\n 795|            cwd: *const u16,\n...\n 824|            std::ptr::null_mut(),\n*825|            0, // bInheritHandles = FALSE — the whole point.\n 826|            BASE_FLAGS | extra_flags,\n 827|            std::ptr::null_mut(), // inherit this process's environment\n 828|            std::ptr::null(),\n...\n 850|#[cfg(windows)]\n*851|pub(crate) fn detached_no_inherit(program: &str, args: &[String]) -> io::Result<u32> {\n 852|    // Try to break away first; if the launching Job Object forbids breakaway the\n 853|    // call fails ACCESS_DENIED → retry in-job so we never regress a spawn that\n 854|    // worked before (just can't escape that job; logged for diagnosis).\n...\n 1442|    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the MECHANISM proof: a child spawned\n*1443|    // by the real `detached_no_inherit` (CREATE_BREAKAWAY_FROM_JOB) ESCAPES a\n 1444|    // KILL_ON_JOB_CLOSE job, so terminating the job does NOT reap it — the OS\n 1445|    // contract the daemon's tab-close survival relies on. The inverse of reap.rs's\n 1446|    // `job_reaps_enrolled_child_and_grandchild` (no breakaway → reaped). We re-run\n 1447|    // THIS test binary as a launcher INSIDE the job (the \"terminal\" stand-in); the\n*1448|    // launcher spawns a long-lived grandchild via `detached_no_inherit`; killing\n 1449|    // the job must leave the grandchild ALIVE.\n 1450|    #[cfg(windows)]\n 1451|    #[test]\n...\n 1511|            }\n*1512|            let gc = detached_no_inherit(\n 1513|                \"ping\",\n 1514|                &[\"-n\".into(), \"300\".into(), \"127.0.0.1\".into()],\n 1515|            )\n...\n 1581|            // than false-fail — the no-regression guarantee is covered by\n*1582|            // `detached_no_inherit_falls_back_under_a_breakaway_denying_job`, and the\n 1583|            // real-terminal escape is an operator real-env acceptance step (a job\n 1584|            // that PERMITS breakaway, which cargo's runner job does not).\n 1585|            Err(e) if e.raw_os_error() == Some(5) => {\n...\n 1652|        // `job_escape_e2e`, and the no-regression fallback by\n*1653|        // `detached_no_inherit_falls_back_under_a_breakaway_denying_job`.\n 1654|        if diag.contains(\"gc_in_any_job=1\") {\n 1655|            eprintln!(\n 1656|                \"SKIP breakaway_spawn_escapes_a_kill_on_close_job: runner ancestor \\\n...\n 1671|    // that forbids breakaway makes the first CreateProcessW attempt fail\n*1672|    // ACCESS_DENIED; `detached_no_inherit` must fall back to the in-job spawn and\n 1673|    // still return a live pid. The shared CI runner runs this test INSIDE exactly\n 1674|    // such a breakaway-denying job, so the call here exercises the fallback path\n 1675|    // directly (on a dev box with no enclosing job the first attempt simply\n...\n 1678|    #[test]\n*1679|    fn detached_no_inherit_falls_back_under_a_breakaway_denying_job() {\n 1680|        use spt_store::proc::is_process_alive;\n*1681|        let pid = detached_no_inherit(\"ping\", &[\"-n\".into(), \"3\".into(), \"127.0.0.1\".into()])\n 1682|            .expect(\"spawn must succeed whether or not breakaway is permitted\");\n 1683|        assert!(pid != 0, \"a real child pid\");\n 1684|        // It is a live, detached process (the fallback did not silently no-op).\n### deelevate.rs\n 32|//! they are re-declared here because the dependency arrow points the other\n*33|//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\n 34|//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\n 35|//! the MSVC default link set) — no new dependency for one seam.\n 36|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n...\n 417|\n*418|    // Raw advapi32/kernel32 declarations (the detached_no_inherit posture:\n 419|    // both libraries are in the MSVC default link set, no new dependency).\n 420|    #[repr(C)]\n 421|    struct StartupInfoW {\n...\n 510|    // CreateProcessWithTokenW accepts only a RESTRICTED creation-flag set —\n*511|    // DETACHED_PROCESS and CREATE_NO_WINDOW (the detached_no_inherit combo)\n 512|    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second\n 513|    // elevated verification round). Detach therefore rides\n 514|    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup\n...\n 521|    /// tab-close does not reap the de-elevated daemon (see the unelevated path in\n*522|    /// `daemon::detached_no_inherit`). CreateProcessWithTokenW accepts only a\n 523|    /// RESTRICTED creation-flag set, so this flag may be rejected with\n 524|    /// ERROR_INVALID_PARAMETER (87) — `create_with_token` retries WITHOUT it in\n 525|    /// that case (the elevated daemon then relies on the no-job common case /\n...\n 687|    /// `CreateProcessWithTokenW` under the same flags as\n*688|    /// `detached_no_inherit`, command line quoted by the shared\n 689|    /// [`quote_windows_arg`] rules. The environment is BUILT FOR THE TOKEN\n 690|    /// (`CreateEnvironmentBlock`) — inheriting the elevated caller's\n 691|    /// variables would point the child at the wrong `%LOCALAPPDATA%`\n### shellhost.rs\n 185|            #[cfg(windows)]\n*186|            let pid = crate::daemon::detached_no_inherit(program, args)\n 187|                .map_err(|e| format!(\"spawn {program}: {e}\"))?;\n 188|            #[cfg(unix)]\n 189|            let pid = Command::new(program)"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":25,"fileCount":4,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/shellhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":1},{"path":"crates/spt-daemon/src/daemon.rs","count":18},{"path":"crates/spt-daemon/src/deelevate.rs","count":5},{"path":"crates/spt-daemon/src/shellhost.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/\n## Cargo.toml\n 55│# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n*56│# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n 57│[target.'cfg(unix)'.dependencies]\n 58│libc = \"0.2\"\n 59│\n\n## src/\n### daemon.rs\n  618│/// harness hook reading the command's output). On Windows `CreateProcess` runs\n *619│/// with `bInheritHandles = TRUE`, so the long-lived daemon would inherit that\n  620│/// pipe's write end and hold it open forever — the capturing reader never sees\n  621│/// EOF and hangs. Worse, inheritable pipe handles flow through EVERY\n *622│/// `bInheritHandles = TRUE` hop: stripping only this process's *std* handles\n  623│/// (the first guard shipped here) still leaked a grandparent capture's pipe\n  624│/// sitting in our handle table as a stray. So detached long-lived children are\n *625│/// spawned with **`bInheritHandles = FALSE`** ([`detached_no_inherit`]) — zero\n  626│/// handles flow, whatever the ancestry. Unix is immune (Rust marks pipe fds\n  627│/// `CLOEXEC`) and keeps the plain `Command` path.\n  628│///\n     │...\n  632│/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n *633│/// no handle inheritance by construction, so 5.6 holds on that path too),\n  634│/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n  635│/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n  636│/// spawn proceeds as-is with a loud warning — a consistent universe, never a\n     │...\n  729│/// SILENT_BREAKAWAY_OK) or there is no job; a job that FORBIDS breakaway fails\n *730│/// `CreateProcessW` with ERROR_ACCESS_DENIED. Used by [`detached_no_inherit`]'s\n  731│/// fallback rung and, for the daemon, the [`launch_daemon_job_neutral`] ladder.\n  732│#[cfg(windows)]\n  733│const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;\n     │...\n  738│/// handles, with `extra_flags` OR'd onto the base detached flags. The single\n *739│/// FFI primitive shared by [`detached_no_inherit`] and the daemon's job-neutral\n  740│/// launcher ladder ([`launch_daemon_job_neutral`]).\n  741│///\n *742│/// **`bInheritHandles = FALSE`** is the KH 5.6 guard in its complete form: no\n  743│/// handle (std or stray grandparent pipe) flows into the immortal child, so no\n  744│/// capturing caller anywhere up the process chain can be wedged by it.\n  745│/// `DETACHED_PROCESS` ⇒ no console, so the child's std handles are simply absent\n     │...\n  791│            thread_attrs: *mut core::ffi::c_void,\n *792│            inherit_handles: i32,\n  793│            flags: u32,\n  794│            env: *mut core::ffi::c_void,\n  795│            cwd: *const u16,\n     │...\n  824│            std::ptr::null_mut(),\n *825│            0, // bInheritHandles = FALSE — the whole point.\n  826│            BASE_FLAGS | extra_flags,\n  827│            std::ptr::null_mut(), // inherit this process's environment\n  828│            std::ptr::null(),\n     │...\n  850│#[cfg(windows)]\n *851│pub(crate) fn detached_no_inherit(program: &str, args: &[String]) -> io::Result<u32> {\n  852│    // Try to break away first; if the launching Job Object forbids breakaway the\n  853│    // call fails ACCESS_DENIED → retry in-job so we never regress a spawn that\n  854│    // worked before (just can't escape that job; logged for diagnosis).\n     │...\n 1442│    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the MECHANISM proof: a child spawned\n*1443│    // by the real `detached_no_inherit` (CREATE_BREAKAWAY_FROM_JOB) ESCAPES a\n 1444│    // KILL_ON_JOB_CLOSE job, so terminating the job does NOT reap it — the OS\n 1445│    // contract the daemon's tab-close survival relies on. The inverse of reap.rs's\n 1446│    // `job_reaps_enrolled_child_and_grandchild` (no breakaway → reaped). We re-run\n 1447│    // THIS test binary as a launcher INSIDE the job (the \"terminal\" stand-in); the\n*1448│    // launcher spawns a long-lived grandchild via `detached_no_inherit`; killing\n 1449│    // the job must leave the grandchild ALIVE.\n 1450│    #[cfg(windows)]\n 1451│    #[test]\n     │...\n 1511│            }\n*1512│            let gc = detached_no_inherit(\n 1513│                \"ping\",\n 1514│                &[\"-n\".into(), \"300\".into(), \"127.0.0.1\".into()],\n 1515│            )\n     │...\n 1581│            // than false-fail — the no-regression guarantee is covered by\n*1582│            // `detached_no_inherit_falls_back_under_a_breakaway_denying_job`, and the\n 1583│            // real-terminal escape is an operator real-env acceptance step (a job\n 1584│            // that PERMITS breakaway, which cargo's runner job does not).\n 1585│            Err(e) if e.raw_os_error() == Some(5) => {\n     │...\n 1652│        // `job_escape_e2e`, and the no-regression fallback by\n*1653│        // `detached_no_inherit_falls_back_under_a_breakaway_denying_job`.\n 1654│        if diag.contains(\"gc_in_any_job=1\") {\n 1655│            eprintln!(\n 1656│                \"SKIP breakaway_spawn_escapes_a_kill_on_close_job: runner ancestor \\\n     │...\n 1671│    // that forbids breakaway makes the first CreateProcessW attempt fail\n*1672│    // ACCESS_DENIED; `detached_no_inherit` must fall back to the in-job spawn and\n 1673│    // still return a live pid. The shared CI runner runs this test INSIDE exactly\n 1674│    // such a breakaway-denying job, so the call here exercises the fallback path\n 1675│    // directly (on a dev box with no enclosing job the first attempt simply\n     │...\n 1678│    #[test]\n*1679│    fn detached_no_inherit_falls_back_under_a_breakaway_denying_job() {\n 1680│        use spt_store::proc::is_process_alive;\n*1681│        let pid = detached_no_inherit(\"ping\", &[\"-n\".into(), \"3\".into(), \"127.0.0.1\".into()])\n 1682│            .expect(\"spawn must succeed whether or not breakaway is permitted\");\n 1683│        assert!(pid != 0, \"a real child pid\");\n 1684│        // It is a live, detached process (the fallback did not silently no-op).\n### deelevate.rs\n  32│//! they are re-declared here because the dependency arrow points the other\n *33│//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\n  34│//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\n  35│//! the MSVC default link set) — no new dependency for one seam.\n  36│// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n    │...\n 417│\n*418│    // Raw advapi32/kernel32 declarations (the detached_no_inherit posture:\n 419│    // both libraries are in the MSVC default link set, no new dependency).\n 420│    #[repr(C)]\n 421│    struct StartupInfoW {\n    │...\n 510│    // CreateProcessWithTokenW accepts only a RESTRICTED creation-flag set —\n*511│    // DETACHED_PROCESS and CREATE_NO_WINDOW (the detached_no_inherit combo)\n 512│    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second\n 513│    // elevated verification round). Detach therefore rides\n 514│    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup\n    │...\n 521│    /// tab-close does not reap the de-elevated daemon (see the unelevated path in\n*522│    /// `daemon::detached_no_inherit`). CreateProcessWithTokenW accepts only a\n 523│    /// RESTRICTED creation-flag set, so this flag may be rejected with\n 524│    /// ERROR_INVALID_PARAMETER (87) — `create_with_token` retries WITHOUT it in\n 525│    /// that case (the elevated daemon then relies on the no-job common case /\n    │...\n 687│    /// `CreateProcessWithTokenW` under the same flags as\n*688│    /// `detached_no_inherit`, command line quoted by the shared\n 689│    /// [`quote_windows_arg`] rules. The environment is BUILT FOR THE TOKEN\n 690│    /// (`CreateEnvironmentBlock`) — inheriting the elevated caller's\n 691│    /// variables would point the child at the wrong `%LOCALAPPDATA%`\n### shellhost.rs\n 185│            #[cfg(windows)]\n*186│            let pid = crate::daemon::detached_no_inherit(program, args)\n 187│                .map_err(|e| format!(\"spawn {program}: {e}\"))?;\n 188│            #[cfg(unix)]\n 189│            let pid = Command::new(program)"},"isError":false,"timestamp":1784196001443}}
{"type":"message","id":"a52b8339","parentId":"348a3697","timestamp":"2026-07-16T10:00:01.446Z","message":{"role":"toolResult","toolCallId":"call_Z4LKGoTDSNdbFBoBYcZOCLtA|fc_0698befac979932d016a58aba10880819a8edf9b3215d23833","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### livehost.rs\n 415|/// Re-run an endpoint's harness by resuming its LAST LEDGER session via the adapter's\n*416|/// `[session.resume]` (`launch_harness_brokered_in`, `is_resume=true`) — the shared\n 417|/// action behind BOTH the wake-resume leg ([`resume_woken_endpoint`]) and the\n 418|/// daemon-restart survival pass ([`resume_restart_orphaned_endpoints`]). `reason_tag`\n 419|/// names the trigger in the log breadcrumb (`WAKE` / `DAEMON_RESTART`), scoped so an\n...\n 445|    let node_label = spt_store::hostlabel::node_fill_label(None);\n*446|    match crate::harnesshost::launch_harness_brokered_in(\n 447|        &crate::endpoint::broker_socket_name(),\n 448|        id,\n 449|        adapter,\n...\n 1801|        let spawn = |marker: &str| {\n*1802|            std::process::Command::new(\"cmd\")\n 1803|                .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n 1804|                .spawn()\n 1805|                .expect(\"spawn\")\n...\n 1811|        let spawn = |marker: &str| {\n*1812|            std::process::Command::new(\"sh\")\n 1813|                .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n 1814|                .spawn()\n 1815|                .expect(\"spawn\")\n### shellwake.rs\n 193|    while !stop.load(Ordering::SeqCst) {\n*194|        let mut cmd = Command::new(program);\n 195|        cmd.args(args)\n 196|            .stdin(Stdio::null())\n 197|            .stdout(Stdio::null())\n### lifecycle.rs\n 29|};\n*30|use spt_runtime::{Manifest, ManifestRuntime};\n 31|use spt_store::epoch::EpochSource;\n 32|use spt_store::notif::NotifStore;\n 33|use spt_store::perch::{self, ParentHint};\n...\n 305|\n*306|/// A live endpoint's adapter `Manifest` + its `ManifestRuntime`, behind shared\n 307|/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n 308|/// **refresh** them while the brain driver thread keeps running. The\n 309|/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n...\n 316|    manifest: Arc<RwLock<Manifest>>,\n*317|    runtime: Arc<RwLock<ManifestRuntime>>,\n 318|}\n 319|\n 320|impl ManifestCell {\n...\n 332|    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n*333|    fn runtime_snapshot(&self) -> ManifestRuntime {\n 334|        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n 335|    }\n 336|\n...\n 358|\n*359|/// Build a [`ManifestRuntime`], pinning the adapter install dir when known so a\n 360|/// role's bare program token resolves there before `PATH` (REQ-INSTALL-11).\n 361|// [impl->REQ-INSTALL-11]\n*362|fn build_runtime(manifest: &Manifest, install_dir: Option<&Path>) -> ManifestRuntime {\n 363|    match install_dir {\n*364|        Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),\n*365|        None => ManifestRuntime::new(manifest.clone()),\n 366|    }\n 367|}\n 368|\n\n## spt-runtime/src/\n### lib.rs\n 8|//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and\n*9|//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template\n 10|//!   substitution + spawn-session seam, T2).\n 11|//!\n 12|//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary\n...\n 28|pub use runtime::{\n*29|    resolve_program_in_dir, run_bounded_command, run_bounded_command_in, scrub_identity_env,\n*30|    AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned, IDENTITY_ENV_VARS,\n 31|    INJECT_ECHO_ENV_VARS,\n 32|};\n### runtime.rs\n 4|//! - [`AgentRuntime`] — the **spawn-session seam** M3 reimplements. M2a's\n*5|//!   [`ManifestRuntime`] spawns a local OS process; M3's daemon will spawn the\n 6|//!   session as a loop/PTY child behind the *same* trait, so callers don't\n 7|//!   change (REQ-SEAM-SPAWN).\n 8|//! - [`fill_template`] — opaque-template substitution. spt-core never parses a\n...\n 40|/// daemon startup ([`scrub_identity_env`]) AND unconditionally on every role-spawn\n*41|/// env assembly ([`ManifestRuntime`]'s shared command builder), regardless of any\n 42|/// role's declared `env_remove`. A spawn that NEEDS an identity var (e.g. a hosted\n 43|/// harness) receives it by explicit per-spawn injection, never by inheritance.\n 44|// [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n...\n 620|#[derive(Clone)]\n*621|pub struct ManifestRuntime {\n 622|    manifest: Manifest,\n 623|    /// The adapter's install dir (`AdapterRecord.source_dir`, or the manifest\n 624|    /// file's own dir on the runtime-manifest seam). When set, a role command's\n...\n 662|\n*663|impl ManifestRuntime {\n 664|    pub fn new(manifest: Manifest) -> Self {\n*665|        ManifestRuntime {\n 666|            manifest,\n 667|            install_dir: None,\n 668|            adapter_name: None,\n...\n 677|    pub fn with_install_dir(manifest: Manifest, install_dir: impl Into<PathBuf>) -> Self {\n*678|        ManifestRuntime {\n 679|            manifest,\n 680|            install_dir: Some(install_dir.into()),\n 681|            adapter_name: None,\n...\n 763|        };\n*764|        let mut cmd = Command::new(&program);\n 765|        cmd.args(args);\n 766|        if let Some(cwd) = &role.cwd {\n 767|            cmd.current_dir(fill_template(cwd, keys)?);\n...\n 804|        // a visible blank conhost on the desktop. Setting it here — the one\n*805|        // shared builder — makes KNOWN-HAZARDS 5.8 hold for every ManifestRuntime\n*806|        // spawn, not just the free run_bounded_command leg.\n 807|        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\n 808|        #[cfg(windows)]\n 809|        {\n...\n 816|\n*817|impl AgentRuntime for ManifestRuntime {\n 818|    fn spawn_session(\n 819|        &self,\n 820|        role: &str,\n...\n 945|/// `fetcher` / `normalize_command` strings through: those are adapter command\n*946|/// lines that live *outside* the role set, so [`ManifestRuntime::run_bounded`]\n 947|/// (which looks a role up in the manifest) does not apply. `stdin` feeds a\n 948|/// `normalize_command` the raw transcript on its standard input; it is written\n 949|/// on a thread so a child that reads slowly never deadlocks the poll.\n 950|// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]\n*951|pub fn run_bounded_command(\n 952|    template: &str,\n 953|    keys: &BTreeMap<String, String>,\n 954|    timeout: Duration,\n...\n 956|) -> Result<BoundedOutput, RuntimeError> {\n*957|    run_bounded_command_in(template, keys, timeout, stdin, None)\n 958|}\n 959|\n 960|/// Resolve a command template's **program token** (first whitespace-split token)\n...\n 991|\n*992|/// As [`run_bounded_command`], but when `install_dir` is `Some`, the template's\n 993|/// program token is first resolved against that adapter install dir\n 994|/// ([`resolve_program_in_dir`]) so a bare program name binds to the adapter's\n 995|/// shipped binary before `PATH` (REQ-INSTALL-11). `None` preserves the legacy\n...\n 998|// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]\n*999|pub fn run_bounded_command_in(\n 1000|    template: &str,\n 1001|    keys: &BTreeMap<String, String>,\n 1002|    timeout: Duration,\n...\n 1016|    };\n*1017|    let mut cmd = Command::new(&program);\n 1018|    cmd.args(args)\n 1019|        .stdin(if stdin.is_some() {\n 1020|            Stdio::piped()\n...\n 1171|        // No default set → inherit the spawner's cwd (today's behavior).\n*1172|        let rt = ManifestRuntime::new(plain.clone());\n 1173|        let cmd = rt\n 1174|            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1175|            .unwrap();\n...\n 1178|        // Default set + role declares no cwd → the default applies.\n*1179|        let rt = ManifestRuntime::new(plain).with_spawn_cwd(Some(default_dir.clone()));\n 1180|        let cmd = rt\n 1181|            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1182|            .unwrap();\n...\n 1196|        .unwrap();\n*1197|        let rt = ManifestRuntime::new(with_cwd).with_spawn_cwd(Some(default_dir));\n 1198|        let cmd = rt\n 1199|            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1200|            .unwrap();\n...\n 1241|        // No stamps → the var is absent from the command's explicit env.\n*1242|        let plain = ManifestRuntime::new(m.clone());\n 1243|        let role = plain.role(\"psyche_resume\").unwrap();\n 1244|        let cmd = plain.command_for(role, &BTreeMap::new()).unwrap();\n 1245|        assert!("}],"details":{"scopePath":"crates/spt-daemon/src/livehost.rs, crates/spt-daemon/src/shellwake.rs, crates/spt-daemon/src/lifecycle.rs, crates/spt-runtime/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":36,"fileCount":5,"files":["crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/shellwake.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-runtime/src/lib.rs","crates/spt-runtime/src/runtime.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/livehost.rs","count":4},{"path":"crates/spt-daemon/src/shellwake.rs","count":1},{"path":"crates/spt-daemon/src/lifecycle.rs","count":8},{"path":"crates/spt-runtime/src/lib.rs","count":3},{"path":"crates/spt-runtime/src/runtime.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/src/\n### livehost.rs\n  415│/// Re-run an endpoint's harness by resuming its LAST LEDGER session via the adapter's\n *416│/// `[session.resume]` (`launch_harness_brokered_in`, `is_resume=true`) — the shared\n  417│/// action behind BOTH the wake-resume leg ([`resume_woken_endpoint`]) and the\n  418│/// daemon-restart survival pass ([`resume_restart_orphaned_endpoints`]). `reason_tag`\n  419│/// names the trigger in the log breadcrumb (`WAKE` / `DAEMON_RESTART`), scoped so an\n     │...\n  445│    let node_label = spt_store::hostlabel::node_fill_label(None);\n *446│    match crate::harnesshost::launch_harness_brokered_in(\n  447│        &crate::endpoint::broker_socket_name(),\n  448│        id,\n  449│        adapter,\n     │...\n 1801│        let spawn = |marker: &str| {\n*1802│            std::process::Command::new(\"cmd\")\n 1803│                .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n 1804│                .spawn()\n 1805│                .expect(\"spawn\")\n     │...\n 1811│        let spawn = |marker: &str| {\n*1812│            std::process::Command::new(\"sh\")\n 1813│                .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n 1814│                .spawn()\n 1815│                .expect(\"spawn\")\n### shellwake.rs\n 193│    while !stop.load(Ordering::SeqCst) {\n*194│        let mut cmd = Command::new(program);\n 195│        cmd.args(args)\n 196│            .stdin(Stdio::null())\n 197│            .stdout(Stdio::null())\n### lifecycle.rs\n  29│};\n *30│use spt_runtime::{Manifest, ManifestRuntime};\n  31│use spt_store::epoch::EpochSource;\n  32│use spt_store::notif::NotifStore;\n  33│use spt_store::perch::{self, ParentHint};\n    │...\n 305│\n*306│/// A live endpoint's adapter `Manifest` + its `ManifestRuntime`, behind shared\n 307│/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can\n 308│/// **refresh** them while the brain driver thread keeps running. The\n 309│/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is\n    │...\n 316│    manifest: Arc<RwLock<Manifest>>,\n*317│    runtime: Arc<RwLock<ManifestRuntime>>,\n 318│}\n 319│\n 320│impl ManifestCell {\n    │...\n 332│    /// [`refresh`](Self::refresh) is never blocked by a long spawn.\n*333│    fn runtime_snapshot(&self) -> ManifestRuntime {\n 334│        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n 335│    }\n 336│\n    │...\n 358│\n*359│/// Build a [`ManifestRuntime`], pinning the adapter install dir when known so a\n 360│/// role's bare program token resolves there before `PATH` (REQ-INSTALL-11).\n 361│// [impl->REQ-INSTALL-11]\n*362│fn build_runtime(manifest: &Manifest, install_dir: Option<&Path>) -> ManifestRuntime {\n 363│    match install_dir {\n*364│        Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),\n*365│        None => ManifestRuntime::new(manifest.clone()),\n 366│    }\n 367│}\n 368│\n\n## spt-runtime/src/\n### lib.rs\n  8│//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and\n *9│//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template\n 10│//!   substitution + spawn-session seam, T2).\n 11│//!\n 12│//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary\n   │...\n 28│pub use runtime::{\n*29│    resolve_program_in_dir, run_bounded_command, run_bounded_command_in, scrub_identity_env,\n*30│    AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned, IDENTITY_ENV_VARS,\n 31│    INJECT_ECHO_ENV_VARS,\n 32│};\n### runtime.rs\n    4│//! - [`AgentRuntime`] — the **spawn-session seam** M3 reimplements. M2a's\n   *5│//!   [`ManifestRuntime`] spawns a local OS process; M3's daemon will spawn the\n    6│//!   session as a loop/PTY child behind the *same* trait, so callers don't\n    7│//!   change (REQ-SEAM-SPAWN).\n    8│//! - [`fill_template`] — opaque-template substitution. spt-core never parses a\n     │...\n   40│/// daemon startup ([`scrub_identity_env`]) AND unconditionally on every role-spawn\n  *41│/// env assembly ([`ManifestRuntime`]'s shared command builder), regardless of any\n   42│/// role's declared `env_remove`. A spawn that NEEDS an identity var (e.g. a hosted\n   43│/// harness) receives it by explicit per-spawn injection, never by inheritance.\n   44│// [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n     │...\n  620│#[derive(Clone)]\n *621│pub struct ManifestRuntime {\n  622│    manifest: Manifest,\n  623│    /// The adapter's install dir (`AdapterRecord.source_dir`, or the manifest\n  624│    /// file's own dir on the runtime-manifest seam). When set, a role command's\n     │...\n  662│\n *663│impl ManifestRuntime {\n  664│    pub fn new(manifest: Manifest) -> Self {\n *665│        ManifestRuntime {\n  666│            manifest,\n  667│            install_dir: None,\n  668│            adapter_name: None,\n     │...\n  677│    pub fn with_install_dir(manifest: Manifest, install_dir: impl Into<PathBuf>) -> Self {\n *678│        ManifestRuntime {\n  679│            manifest,\n  680│            install_dir: Some(install_dir.into()),\n  681│            adapter_name: None,\n     │...\n  763│        };\n *764│        let mut cmd = Command::new(&program);\n  765│        cmd.args(args);\n  766│        if let Some(cwd) = &role.cwd {\n  767│            cmd.current_dir(fill_template(cwd, keys)?);\n     │...\n  804│        // a visible blank conhost on the desktop. Setting it here — the one\n *805│        // shared builder — makes KNOWN-HAZARDS 5.8 hold for every ManifestRuntime\n *806│        // spawn, not just the free run_bounded_command leg.\n  807│        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\n  808│        #[cfg(windows)]\n  809│        {\n     │...\n  816│\n *817│impl AgentRuntime for ManifestRuntime {\n  818│    fn spawn_session(\n  819│        &self,\n  820│        role: &str,\n     │...\n  945│/// `fetcher` / `normalize_command` strings through: those are adapter command\n *946│/// lines that live *outside* the role set, so [`ManifestRuntime::run_bounded`]\n  947│/// (which looks a role up in the manifest) does not apply. `stdin` feeds a\n  948│/// `normalize_command` the raw transcript on its standard input; it is written\n  949│/// on a thread so a child that reads slowly never deadlocks the poll.\n  950│// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]\n *951│pub fn run_bounded_command(\n  952│    template: &str,\n  953│    keys: &BTreeMap<String, String>,\n  954│    timeout: Duration,\n     │...\n  956│) -> Result<BoundedOutput, RuntimeError> {\n *957│    run_bounded_command_in(template, keys, timeout, stdin, None)\n  958│}\n  959│\n  960│/// Resolve a command template's **program token** (first whitespace-split token)\n     │...\n  991│\n *992│/// As [`run_bounded_command`], but when `install_dir` is `Some`, the template's\n  993│/// program token is first resolved against that adapter install dir\n  994│/// ([`resolve_program_in_dir`]) so a bare program name binds to the adapter's\n  995│/// shipped binary before `PATH` (REQ-INSTALL-11). `None` preserves the legacy\n     │...\n  998│// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]\n *999│pub fn run_bounded_command_in(\n 1000│    template: &str,\n 1001│    keys: &BTreeMap<String, String>,\n 1002│    timeout: Duration,\n     │...\n 1016│    };\n*1017│    let mut cmd = Command::new(&program);\n 1018│    cmd.args(args)\n 1019│        .stdin(if stdin.is_some() {\n 1020│            Stdio::piped()\n     │...\n 1171│        // No default set → inherit the spawner's cwd (today's behavior).\n*1172│        let rt = ManifestRuntime::new(plain.clone());\n 1173│        let cmd = rt\n 1174│            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1175│            .unwrap();\n     │...\n 1178│        // Default set + role declares no cwd → the default applies.\n*1179│        let rt = ManifestRuntime::new(plain).with_spawn_cwd(Some(default_dir.clone()));\n 1180│        let cmd = rt\n 1181│            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1182│            .unwrap();\n     │...\n 1196│        .unwrap();\n*1197│        let rt = ManifestRuntime::new(with_cwd).with_spawn_cwd(Some(default_dir));\n 1198│        let cmd = rt\n 1199│            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1200│            .unwrap();\n     │...\n 1241│        // No stamps → the var is absent from the command's explicit env.\n*1242│        let plain = ManifestRuntime::new(m.clone());\n 1243│        let role = plain.role(\"psyche_resume\").unwrap();\n 1244│        let cmd = plain.command_for(role, &BTreeMap::new()).unwrap();\n 1245│        assert!("},"isError":false,"timestamp":1784196001446}}
{"type":"message","id":"b4662add","parentId":"a52b8339","timestamp":"2026-07-16T10:00:01.460Z","message":{"role":"toolResult","toolCallId":"call_bYRLa42B0iGrAnsGOvzrwnAg|fc_0698befac979932d016a58aba108b4819a95caebddf6af5843","toolName":"read","content":[{"type":"text","text":"/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`\n/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            finished: false,\n            retentive_cap: 0,\n        }\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Attach `sub` as the subscriber, replaying every retained chunk with\n    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        self.subscriber = Some(Arc::clone(&sub));\n        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n        if self.finished {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if Arc::ptr_eq(cur, sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A bounded, sequenced log of connection liveness events — the broker-owned\n…\n    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {\n…\n        let owner_stream = self.open_stream(conn_id)?;\n        let shell_stream = self\n            .inbound_rows_on(conn_id)\n            .difference(&before)\n            .copied()\n            .next()\n            .ok_or_else(|| io::Error::other(\"loopback peer row not registered\"))?;\n        Ok((owner_stream, shell_stream))\n    }\n\n    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n    // [impl->REQ-DAEMON-2]\n    pub(crate) fn subscribe_stream(\n        &self,\n        stream_id: u64,\n        sub: SharedSend,\n        from_seq: u64,\n    ) -> io::Result<()> {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            let e = streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?;\n            Arc::clone(&e.log)\n        };\n        log.lock().unwrap().attach(sub, from_seq);\n        Ok(())\n    }\n\n    /// Detach `sub` from a stream iff it is the current subscriber (brain\n    /// disconnect cleanup; the stream + its ring persist).\n    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n        };\n        if let Some(log) = log {\n            log.lock().unwrap().detach_if(sub);\n        }\n    }\n\n    /// Attach `sub` as the presence subscriber, replaying liveness events from"}],"details":{"displayContent":{"text":"/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`\n/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            finished: false,\n            retentive_cap: 0,\n        }\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Attach `sub` as the subscriber, replaying every retained chunk with\n    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        self.subscriber = Some(Arc::clone(&sub));\n        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n        if self.finished {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if Arc::ptr_eq(cur, sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A bounded, sequenced log of connection liveness events — the broker-owned\n…\n    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {\n…\n        let owner_stream = self.open_stream(conn_id)?;\n        let shell_stream = self\n            .inbound_rows_on(conn_id)\n            .difference(&before)\n            .copied()\n            .next()\n            .ok_or_else(|| io::Error::other(\"loopback peer row not registered\"))?;\n        Ok((owner_stream, shell_stream))\n    }\n\n    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n    // [impl->REQ-DAEMON-2]\n    pub(crate) fn subscribe_stream(\n        &self,\n        stream_id: u64,\n        sub: SharedSend,\n        from_seq: u64,\n    ) -> io::Result<()> {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            let e = streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?;\n            Arc::clone(&e.log)\n        };\n        log.lock().unwrap().attach(sub, from_seq);\n        Ok(())\n    }\n\n    /// Detach `sub` from a stream iff it is the current subscriber (brain\n    /// disconnect cleanup; the stream + its ring persist).\n    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n        };\n        if let Some(log) = log {\n            log.lock().unwrap().detach_if(sub);\n        }\n    }\n\n    /// Attach `sub` as the presence subscriber, replaying liveness events from","startLine":150,"lineNumbers":[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,null,1486,null,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784196001460}}
{"type":"message","id":"ccf730fc","parentId":"b4662add","timestamp":"2026-07-16T10:00:01.463Z","message":{"role":"toolResult","toolCallId":"call_YkxopEr67opVYXBj9LapnRRB|fc_0698befac979932d016a58aba108a4819a8221084e2df70698","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### livehost.rs\n 53|/// held stop flag (tripped to un-host on an offline-transition). F-030 W3: the\n*54|/// per-event psyche driver spawns NO resident child, so this holds no psyche\n 55|/// handle and no residency anchor — a psyche is a bounded per-event turn, never a\n 56|/// process to own/reap (the resident residency/reap machinery retired here).\n 57|struct HostedLife {\n...\n 124|            let _ = h.thread.join();\n*125|            // F-030 W3: nothing detached to reap — the per-event psyche driver spawns\n 126|            // no resident child, so joining the driver thread + de-tabling above is the\n 127|            // whole un-host (the resident-psyche handle-reap + orphan-reap retired with\n 128|            // the residency machinery). Un-hosting a genuinely-offline endpoint is legit.\n...\n 614|/// config-paced pulse driver on its own supervised thread with a held stop flag.\n*615|/// A Psyche-spawn failure tables nothing — the next sweep retries.\n 616|// [impl->REQ-DAEMON-1]\n 617|fn host_one(\n 618|    set: &Arc<LiveSet>,\n...\n 624|    lifecycle.sweep_on_start();\n*625|    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n 626|    // (`BrainLifecycle::run_psyche_event_turn` on each armed pulse fire), never a\n 627|    // long-lived process — so the child never self-resolves home/subnet and the\n 628|    // direction-(a) multi-subnet poll-refusal churn is impossible by construction.\n...\n 895|/// F-030 W5 ONE-SHOT brain-start GC of stranded `.live-bin` own-copy dirs (design\n*896|/// §W5, [`REQ-PSYCHE-LEGACY-RESIDENT-SWEEP`]). The retired resident spawn kept a\n 897|/// per-endpoint `<perch>/.live-bin/<prog>` PRIVATE COPY of the psyche binary; the\n 898|/// ephemeral per-event turn needs none (it runs from the install-dir/PATH snapshot,\n 899|/// image-lock exposure now turn-bounded), so a cross-version upgrade leaves a multi-MB\n...\n 1063|\n*1064|    // A no-op Psyche summarizer command: spawns + exits 0 (we never depend on its\n 1065|    // output — the daemon online-stamp is what hosts it, not the pid).\n 1066|    #[cfg(windows)]\n 1067|    const NOOP_CMD: &str = \"cmd /C rem\";\n...\n 1361|            // is_perch_alive true, and the adapterless check makes the host loop skip it\n*1362|            // (no psyche spawn), so the test stays hermetic.\n 1363|            let lp = perch::resolve_perch_path(\"liveparent\", ParentHint::Infer);\n 1364|            std::fs::create_dir_all(&lp).unwrap();\n 1365|            spt_store::info::write_info(\n...\n 1555|    // [unit->REQ-DAEMON-1] (a) FIRST-HOST scan: an online live endpoint on disk is\n*1556|    // hosted by the reconcile — the Psyche is spawned AND the config-paced pulse\n 1557|    // driver actually runs (it stamps last_active_ms on the Self perch each tick).\n 1558|    #[test]\n 1559|    fn reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse() {\n...\n 1736|    // endpoint whose info.json becomes momentarily UNREADABLE while its perch DIR\n*1737|    // remains is NOT un-hosted (transient → keep the driver, no Psyche stop/respawn\n 1738|    // churn on a disk blip); only a GONE perch dir (torn down) un-hosts.\n 1739|    #[test]\n 1740|    fn reconcile_keeps_driver_on_transient_read_fail_unhosts_on_dir_gone() {\n...\n 1803|                .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n*1804|                .spawn()\n 1805|                .expect(\"spawn\")\n 1806|        };\n 1807|        // A trailing shell builtin (`:`) keeps the shell RESIDENT (no tail-exec-replace by\n...\n 1813|                .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n*1814|                .spawn()\n 1815|                .expect(\"spawn\")\n 1816|        };\n*1817|        let mut psyche = spawn(\"agent7-psyche\"); // the real {id} legacy wrapper\n 1818|        let mut sibling = spawn(\"agent7-other-psyche\"); // a sibling agent, same basename\n 1819|\n 1820|        // Wait until the real wrapper's cmdline is queryable + carries its marker.\n### lifecycle.rs\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...\n 85|/// Write the psyche context file for THIS turn — CORE owns the lifecycle\n*86|/// (REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): write-before-spawn each turn, OVERWRITE IN\n 87|/// PLACE, persists between turns in the nested perch (debuggability), NEVER deleted.\n 88|/// `content` is the composed mind on a FRESH (FreshWithPreload) turn — NON-EMPTY via\n 89|/// [`first_turn_psyche_context`] — or EXACTLY the empty string on a CONTINUE\n...\n 188|    // semantics live in the file CONTENT (non-empty=fresh, 0-byte=continue), written\n*189|    // by `write_psyche_context_file` before the spawn. The shim reads-file-then-trims.\n 190|    base.insert(\n 191|        \"psyche_context_file\".to_string(),\n 192|        context_file.to_string_lossy().to_string(),\n...\n 343|    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n*344|    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n 345|    /// brain thread, PTY, broker session, and output log are untouched; only these\n 346|    /// two cells' contents change. (W3c)\n 347|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n...\n 839|    /// The dir routes through [`resolve_endpoint_drop_dir`] — the SAME resolver the\n*840|    /// ingest leg and `psyche_drop_file` already use — and the summarizer spawn gets\n 841|    /// the endpoint's cwd as its default working dir (a role-declared cwd wins).\n 842|    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n 843|    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n...\n 1004|        //\n*1005|        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n 1006|        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n*1007|        // e.g. a ccs-relocated CLAUDE_CONFIG_DIR) into the psyche_resume spawn env. The\n 1008|        // daemon spawns from BARE env, so without this a profile-relocated parent's\n 1009|        // psyche lands in the DEFAULT account root → headless \"Not logged in\" → strike\n 1010|        // loop (flynn's field death). Harness-agnostic: whatever was captured is\n...\n 1368|    // A live-capable manifest: a Psyche init template + a commune dir to watch.\n*1369|    // The Psyche command is a trivial no-op (we never spawn it in these tests;\n 1370|    // we exercise the pulse/ingest loop, which does not launch the Psyche).\n 1371|    #[cfg(windows)]\n 1372|    const NOOP_CMD: &str = \"cmd /C rem\";\n...\n 1428|    /// Seed the PARENT perch record for `id` with a captured `read_env` stamp — the\n*1429|    /// `[env] direction=\"read\"` value `run_psyche_event_turn` threads into the spawn.\n 1430|    fn seed_parent_with_read_env(id: &str, var: &str, val: &str) {\n 1431|        use spt_store::info::{write_info, InfoJson};\n 1432|        let p = perch::resolve_perch_path(id, ParentHint::Infer);\n...\n 1465|\n*1466|    // [int->REQ-PSYCHE-SPAWN-ENV-PARITY] the REAL daemon path (v0.12.1 real-harness\n 1467|    // rule): `run_psyche_event_turn` reads the PARENT record's captured `read_env` and\n*1468|    // threads it onto the `psyche_resume` SPAWN env, so the spawned child SEES the\n 1469|    // stamp. Guards the one-token wiring traps the runtime unit can't — read_info of\n 1470|    // the PARENT (not the worker/psyche perch), the pre-turn snapshot, and the\n 1471|    // parent-perch resolution. A per-platform script writes the threaded env value to\n...\n 1478|    #[test]\n*1479|    fn psyche_turn_threads_parent_read_env_into_spawn() {\n 1480|        with_home(|home| {\n 1481|            let marker = home.join(\"psyche_env_marker.txt\");\n 1482|            let stamp = \"sentinel-acct-root-9f3a\";\n...\n 1504|            let got = std::fs::read_to_string(&marker)\n*1505|                .expect(\"the psyche child must have spawned + written the marker\");\n 1506|            assert_eq!(\n 1507|                got.trim(),\n 1508|                stamp,\n...\n 2260|    // NOTE (F-030 W5): the daemon-crate `daemon_hosted_psyche_liveness_is_status_not_pid`\n*2261|    // unit was removed with the retired `BrainLifecycle::spawn_psyche`. The\n 2262|    // REQ-HAZARD-DAEMON-HOSTED-LIVENESS `unit` stage stays covered by the status-vs-pid\n 2263|    // liveness units in spt-store (liveness.rs / info.rs / registry.rs); the `int` stage\n 2264|    // is the daemon E2E (daemon_e2e.rs), which binds the hosted psyche perch directly.\n...\n 2883|    // NOTE (F-030 W5): the `.live-bin` psyche own-copy tests (copy_if_changed +\n*2884|    // psyche_spawn_runtime own-copy/degrade) were removed with the retired own-copy\n 2885|    // machinery — a per-event psyche turn is TURN-BOUNDED (same image-lock class as\n 2886|    // translate/digest) and spawns from the install-dir/PATH snapshot, so no\n 2887|    // per-endpoint copy is needed. REQ-INSTALL-11's `unit` stage stays covered by the\n\n## spt-runtime/src/\n### manifest.rs\n 480|    /// program token resolves against `install_dir` like `[digest].extractor` /\n*481|    /// `[session.psyche_resume]`. The spawn + stdin/stdout JSON-lines protocol is\n 482|    /// unchanged — `command` only alters how the executable+args are located.\n 483|    /// Folds `claude-spt translate` into the one consolidated adapter binary.\n 484|    // [impl->REQ-TRANSLATE-COMMAND]\n...\n 1245|/// them — refusing a dead key prevents no field failure (F-030 retired the resident\n*1246|/// `psyche_init` spawn; the per-event `psyche_resume` replaced it).\n 1247|const RETIRED_UNSPAWNED_ROLES: &[&str] = &[\"psyche_init\"];\n 1248|\n 1249|#[cfg(test)]\n### runtime.rs\n 93|    // template that still needs the parent's sid never aliases `{session_id}` (which\n*94|    // is the psyche's own custody sid on a psyche spawn — REQ-PSYCHE-SID-CUSTODY).\n 95|    \"parent_session_id\",\n 96|    \"session_name\",\n 97|    // This node's advertised label (REQ-MANIFEST-NODE-KEY): the value the registry\n...\n 113|    // are GONE — they had no fill site once the per-event psyche_resume turn replaced\n*114|    // the resident psyche_init spawn, F-030; a catalog key must have a real fill.)\n 115|    \"psyche_context_file\",\n 116|    \"link_token\",\n 117|    \"source\",\n...\n 634|    /// perch record) to set as PROCESS ENV on every role spawn this runtime makes —\n*635|    /// the F-027 Half-B env-parity contract (REQ-PSYCHE-SPAWN-ENV-PARITY). The\n 636|    /// daemon spawns from BARE env, so a profile-relocated parent (e.g. a ccs\n 637|    /// `CLAUDE_CONFIG_DIR` account root) would otherwise land its psyche in the wrong\n 638|    /// root; forwarding the captured stamps as env keeps the spawn in the parent's\n...\n 694|    /// Thread captured `[env] direction = \"read\"` stamps ([`capture_read_env`]) as\n*695|    /// PROCESS ENV onto every role spawn this runtime makes (REQ-PSYCHE-SPAWN-ENV-PARITY,\n 696|    /// F-027 Half-B). Used by the daemon's per-event psyche turn: it reads the parent\n 697|    /// record's `read_env` and sets it here so the bare-env daemon spawn lands the\n 698|    /// psyche in the parent's captured account root, not the default. A stamped var\n 699|    /// overrides the ambient value. No-op when the map is empty.\n*700|    // [impl->REQ-PSYCHE-SPAWN-ENV-PARITY]\n 701|    pub fn with_spawn_env(\n 702|        mut self,\n 703|        spawn_env: std::collections::BTreeMap<String, String>,\n...\n 733|    /// env_remove, and the recursion guard. Shared by both spawn paths.\n*734|    fn command_for(\n 735|        &self,\n 736|        role: &SessionRole,\n 737|        keys: &BTreeMap<String, String>,\n...\n 777|        }\n*778|        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): set the captured\n 779|        // read-env stamps as PROCESS ENV so a bare-env daemon spawn inherits the\n 780|        // parent's account root (e.g. a relocated CLAUDE_CONFIG_DIR) instead of the\n 781|        // default. AFTER env_remove (a stamped var is never stripped) and BEFORE the\n 782|        // recursion guard. A stamped var OVERRIDES the ambient value. Empty ⇒ no-op.\n*783|        // [impl->REQ-PSYCHE-SPAWN-ENV-PARITY]\n 784|        for (var, val) in &self.spawn_env {\n 785|            cmd.env(var, val);\n 786|        }\n...\n 802|        // the console-less daemon (notif pump) and the detached live-agent\n*803|        // host (psyche/echo/turn): without CREATE_NO_WINDOW each spawn flashes\n 804|        // a visible blank conhost on the desktop. Setting it here — the one\n 805|        // shared builder — makes KNOWN-HAZARDS 5.8 hold for every ManifestRuntime\n 806|        // spawn, not just the free run_bounded_command leg.\n...\n 836|        let role_def = self.role(role)?;\n*837|        let mut cmd = self.command_for(role_def, keys)?;\n 838|        // Discard stdio (the daemon/host is console-less; command_for already set\n 839|        // CREATE_NO_WINDOW on Windows) but RETAIN the child handle so a hosted caller\n 840|        // can reap it BY HANDLE (recycle-proof), unlike the detached spawn above.\n...\n 843|            .stderr(Stdio::null());\n*844|        let child = cmd.spawn().map_err(RuntimeError::Spawn)?;\n 845|        Ok(OwnedSession {\n 846|            pid: child.id(),\n 847|            child,\n...\n 858|        let role_def = self.role(role)?;\n*859|        let mut cmd = self.command_for(role_def, keys)?;\n 860|        cmd.stdin(if stdin.is_some() {\n 861|            Stdio::piped()\n 862|        } else {\n...\n 867|        let label = fill_template(&role_def.command, keys)?;\n*868|        let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;\n 869|        if let Some(bytes) = stdin {\n 870|            if let Some(mut sink) = child.stdin.take() {\n 871|                let owned = bytes.to_vec();\n...\n 1034|    }\n*1035|    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;\n 1036|    if let Some(bytes) = stdin {\n 1037|        if let Some(mut sink) = child.stdin.take() {\n 1038|            let owned = bytes.to_vec();\n...\n 1173|        let cmd = rt\n*1174|            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1175|            .unwrap();\n 1176|        assert_eq!(cmd.get_current_dir(), None, \"no default → inherit\");\n 1177|\n...\n 1180|        let cmd = rt\n*1181|            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1182|            .unwrap();\n 1183|        assert_eq!(\n 1184|            cmd.get_current_dir(),\n...\n 1198|        let cmd = rt\n*1199|            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1200|            .unwrap();\n 1201|        assert_eq!(\n 1202|            cmd.get_current_dir().map(|p| p.to_string_lossy().replace('\\\\', \"/\")),\n...\n 1207|\n*1208|    // ---- REQ-PSYCHE-SPAWN-ENV-PARITY: read-env stamps threaded to the spawn ENV ----\n 1209|\n 1210|    /// The EXPLICITLY-set env vars on a built command (Command::get_envs yields\n 1211|    /// `Some` for a set var, `None` for an env_remove'd one) — the F-027 Half-B\n...\n 1234|\n*1235|    // [unit->REQ-PSYCHE-SPAWN-ENV-PARITY] captured read-env stamps threaded via\n 1236|    // `with_spawn_env` are applied as PROCESS ENV on the built psyche_resume command;\n 1237|    // a runtime with NO stamps sets no such env (the current non-psyche callers).\n 1238|    #[test]\n...\n 1243|        let role = plain.role(\"psyche_resume\").unwrap();\n*1244|        let cmd = plain.command_for(role, &BTreeMap::new()).unwrap();\n 1245|        assert!(\n 1246|            !env_set(&cmd).contains_key(\"CLAUDE_CONFIG_DIR\"),\n 1247|            \"no stamp → no env var set on the spawn\""}],"details":{"scopePath":"crates/spt-daemon/src/livehost.rs, crates/spt-daemon/src/lifecycle.rs, crates/spt-runtime/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":49,"fileCount":4,"files":["crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-runtime/src/manifest.rs","crates/spt-runtime/src/runtime.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/livehost.rs","count":12},{"path":"crates/spt-daemon/src/lifecycle.rs","count":15},{"path":"crates/spt-runtime/src/manifest.rs","count":2},{"path":"crates/spt-runtime/src/runtime.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/src/\n### livehost.rs\n   53│/// held stop flag (tripped to un-host on an offline-transition). F-030 W3: the\n  *54│/// per-event psyche driver spawns NO resident child, so this holds no psyche\n   55│/// handle and no residency anchor — a psyche is a bounded per-event turn, never a\n   56│/// process to own/reap (the resident residency/reap machinery retired here).\n   57│struct HostedLife {\n     │...\n  124│            let _ = h.thread.join();\n *125│            // F-030 W3: nothing detached to reap — the per-event psyche driver spawns\n  126│            // no resident child, so joining the driver thread + de-tabling above is the\n  127│            // whole un-host (the resident-psyche handle-reap + orphan-reap retired with\n  128│            // the residency machinery). Un-hosting a genuinely-offline endpoint is legit.\n     │...\n  614│/// config-paced pulse driver on its own supervised thread with a held stop flag.\n *615│/// A Psyche-spawn failure tables nothing — the next sweep retries.\n  616│// [impl->REQ-DAEMON-1]\n  617│fn host_one(\n  618│    set: &Arc<LiveSet>,\n     │...\n  624│    lifecycle.sweep_on_start();\n *625│    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n  626│    // (`BrainLifecycle::run_psyche_event_turn` on each armed pulse fire), never a\n  627│    // long-lived process — so the child never self-resolves home/subnet and the\n  628│    // direction-(a) multi-subnet poll-refusal churn is impossible by construction.\n     │...\n  895│/// F-030 W5 ONE-SHOT brain-start GC of stranded `.live-bin` own-copy dirs (design\n *896│/// §W5, [`REQ-PSYCHE-LEGACY-RESIDENT-SWEEP`]). The retired resident spawn kept a\n  897│/// per-endpoint `<perch>/.live-bin/<prog>` PRIVATE COPY of the psyche binary; the\n  898│/// ephemeral per-event turn needs none (it runs from the install-dir/PATH snapshot,\n  899│/// image-lock exposure now turn-bounded), so a cross-version upgrade leaves a multi-MB\n     │...\n 1063│\n*1064│    // A no-op Psyche summarizer command: spawns + exits 0 (we never depend on its\n 1065│    // output — the daemon online-stamp is what hosts it, not the pid).\n 1066│    #[cfg(windows)]\n 1067│    const NOOP_CMD: &str = \"cmd /C rem\";\n     │...\n 1361│            // is_perch_alive true, and the adapterless check makes the host loop skip it\n*1362│            // (no psyche spawn), so the test stays hermetic.\n 1363│            let lp = perch::resolve_perch_path(\"liveparent\", ParentHint::Infer);\n 1364│            std::fs::create_dir_all(&lp).unwrap();\n 1365│            spt_store::info::write_info(\n     │...\n 1555│    // [unit->REQ-DAEMON-1] (a) FIRST-HOST scan: an online live endpoint on disk is\n*1556│    // hosted by the reconcile — the Psyche is spawned AND the config-paced pulse\n 1557│    // driver actually runs (it stamps last_active_ms on the Self perch each tick).\n 1558│    #[test]\n 1559│    fn reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse() {\n     │...\n 1736│    // endpoint whose info.json becomes momentarily UNREADABLE while its perch DIR\n*1737│    // remains is NOT un-hosted (transient → keep the driver, no Psyche stop/respawn\n 1738│    // churn on a disk blip); only a GONE perch dir (torn down) un-hosts.\n 1739│    #[test]\n 1740│    fn reconcile_keeps_driver_on_transient_read_fail_unhosts_on_dir_gone() {\n     │...\n 1803│                .args([\"/C\", &format!(\"ping -n 30 127.0.0.1 >NUL & rem {marker}\")])\n*1804│                .spawn()\n 1805│                .expect(\"spawn\")\n 1806│        };\n 1807│        // A trailing shell builtin (`:`) keeps the shell RESIDENT (no tail-exec-replace by\n     │...\n 1813│                .args([\"-c\", &format!(\"sleep 30; : {marker}\")])\n*1814│                .spawn()\n 1815│                .expect(\"spawn\")\n 1816│        };\n*1817│        let mut psyche = spawn(\"agent7-psyche\"); // the real {id} legacy wrapper\n 1818│        let mut sibling = spawn(\"agent7-other-psyche\"); // a sibling agent, same basename\n 1819│\n 1820│        // Wait until the real wrapper's cmdline is queryable + carries its marker.\n### lifecycle.rs\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     │...\n   85│/// Write the psyche context file for THIS turn — CORE owns the lifecycle\n  *86│/// (REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): write-before-spawn each turn, OVERWRITE IN\n   87│/// PLACE, persists between turns in the nested perch (debuggability), NEVER deleted.\n   88│/// `content` is the composed mind on a FRESH (FreshWithPreload) turn — NON-EMPTY via\n   89│/// [`first_turn_psyche_context`] — or EXACTLY the empty string on a CONTINUE\n     │...\n  188│    // semantics live in the file CONTENT (non-empty=fresh, 0-byte=continue), written\n *189│    // by `write_psyche_context_file` before the spawn. The shim reads-file-then-trims.\n  190│    base.insert(\n  191│        \"psyche_context_file\".to_string(),\n  192│        context_file.to_string_lossy().to_string(),\n     │...\n  343│    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n *344│    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n  345│    /// brain thread, PTY, broker session, and output log are untouched; only these\n  346│    /// two cells' contents change. (W3c)\n  347│    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n     │...\n  839│    /// The dir routes through [`resolve_endpoint_drop_dir`] — the SAME resolver the\n *840│    /// ingest leg and `psyche_drop_file` already use — and the summarizer spawn gets\n  841│    /// the endpoint's cwd as its default working dir (a role-declared cwd wins).\n  842│    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n  843│    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n     │...\n 1004│        //\n*1005│        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n 1006│        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n*1007│        // e.g. a ccs-relocated CLAUDE_CONFIG_DIR) into the psyche_resume spawn env. The\n 1008│        // daemon spawns from BARE env, so without this a profile-relocated parent's\n 1009│        // psyche lands in the DEFAULT account root → headless \"Not logged in\" → strike\n 1010│        // loop (flynn's field death). Harness-agnostic: whatever was captured is\n     │...\n 1368│    // A live-capable manifest: a Psyche init template + a commune dir to watch.\n*1369│    // The Psyche command is a trivial no-op (we never spawn it in these tests;\n 1370│    // we exercise the pulse/ingest loop, which does not launch the Psyche).\n 1371│    #[cfg(windows)]\n 1372│    const NOOP_CMD: &str = \"cmd /C rem\";\n     │...\n 1428│    /// Seed the PARENT perch record for `id` with a captured `read_env` stamp — the\n*1429│    /// `[env] direction=\"read\"` value `run_psyche_event_turn` threads into the spawn.\n 1430│    fn seed_parent_with_read_env(id: &str, var: &str, val: &str) {\n 1431│        use spt_store::info::{write_info, InfoJson};\n 1432│        let p = perch::resolve_perch_path(id, ParentHint::Infer);\n     │...\n 1465│\n*1466│    // [int->REQ-PSYCHE-SPAWN-ENV-PARITY] the REAL daemon path (v0.12.1 real-harness\n 1467│    // rule): `run_psyche_event_turn` reads the PARENT record's captured `read_env` and\n*1468│    // threads it onto the `psyche_resume` SPAWN env, so the spawned child SEES the\n 1469│    // stamp. Guards the one-token wiring traps the runtime unit can't — read_info of\n 1470│    // the PARENT (not the worker/psyche perch), the pre-turn snapshot, and the\n 1471│    // parent-perch resolution. A per-platform script writes the threaded env value to\n     │...\n 1478│    #[test]\n*1479│    fn psyche_turn_threads_parent_read_env_into_spawn() {\n 1480│        with_home(|home| {\n 1481│            let marker = home.join(\"psyche_env_marker.txt\");\n 1482│            let stamp = \"sentinel-acct-root-9f3a\";\n     │...\n 1504│            let got = std::fs::read_to_string(&marker)\n*1505│                .expect(\"the psyche child must have spawned + written the marker\");\n 1506│            assert_eq!(\n 1507│                got.trim(),\n 1508│                stamp,\n     │...\n 2260│    // NOTE (F-030 W5): the daemon-crate `daemon_hosted_psyche_liveness_is_status_not_pid`\n*2261│    // unit was removed with the retired `BrainLifecycle::spawn_psyche`. The\n 2262│    // REQ-HAZARD-DAEMON-HOSTED-LIVENESS `unit` stage stays covered by the status-vs-pid\n 2263│    // liveness units in spt-store (liveness.rs / info.rs / registry.rs); the `int` stage\n 2264│    // is the daemon E2E (daemon_e2e.rs), which binds the hosted psyche perch directly.\n     │...\n 2883│    // NOTE (F-030 W5): the `.live-bin` psyche own-copy tests (copy_if_changed +\n*2884│    // psyche_spawn_runtime own-copy/degrade) were removed with the retired own-copy\n 2885│    // machinery — a per-event psyche turn is TURN-BOUNDED (same image-lock class as\n 2886│    // translate/digest) and spawns from the install-dir/PATH snapshot, so no\n 2887│    // per-endpoint copy is needed. REQ-INSTALL-11's `unit` stage stays covered by the\n\n## spt-runtime/src/\n### manifest.rs\n  480│    /// program token resolves against `install_dir` like `[digest].extractor` /\n *481│    /// `[session.psyche_resume]`. The spawn + stdin/stdout JSON-lines protocol is\n  482│    /// unchanged — `command` only alters how the executable+args are located.\n  483│    /// Folds `claude-spt translate` into the one consolidated adapter binary.\n  484│    // [impl->REQ-TRANSLATE-COMMAND]\n     │...\n 1245│/// them — refusing a dead key prevents no field failure (F-030 retired the resident\n*1246│/// `psyche_init` spawn; the per-event `psyche_resume` replaced it).\n 1247│const RETIRED_UNSPAWNED_ROLES: &[&str] = &[\"psyche_init\"];\n 1248│\n 1249│#[cfg(test)]\n### runtime.rs\n   93│    // template that still needs the parent's sid never aliases `{session_id}` (which\n  *94│    // is the psyche's own custody sid on a psyche spawn — REQ-PSYCHE-SID-CUSTODY).\n   95│    \"parent_session_id\",\n   96│    \"session_name\",\n   97│    // This node's advertised label (REQ-MANIFEST-NODE-KEY): the value the registry\n     │...\n  113│    // are GONE — they had no fill site once the per-event psyche_resume turn replaced\n *114│    // the resident psyche_init spawn, F-030; a catalog key must have a real fill.)\n  115│    \"psyche_context_file\",\n  116│    \"link_token\",\n  117│    \"source\",\n     │...\n  634│    /// perch record) to set as PROCESS ENV on every role spawn this runtime makes —\n *635│    /// the F-027 Half-B env-parity contract (REQ-PSYCHE-SPAWN-ENV-PARITY). The\n  636│    /// daemon spawns from BARE env, so a profile-relocated parent (e.g. a ccs\n  637│    /// `CLAUDE_CONFIG_DIR` account root) would otherwise land its psyche in the wrong\n  638│    /// root; forwarding the captured stamps as env keeps the spawn in the parent's\n     │...\n  694│    /// Thread captured `[env] direction = \"read\"` stamps ([`capture_read_env`]) as\n *695│    /// PROCESS ENV onto every role spawn this runtime makes (REQ-PSYCHE-SPAWN-ENV-PARITY,\n  696│    /// F-027 Half-B). Used by the daemon's per-event psyche turn: it reads the parent\n  697│    /// record's `read_env` and sets it here so the bare-env daemon spawn lands the\n  698│    /// psyche in the parent's captured account root, not the default. A stamped var\n  699│    /// overrides the ambient value. No-op when the map is empty.\n *700│    // [impl->REQ-PSYCHE-SPAWN-ENV-PARITY]\n  701│    pub fn with_spawn_env(\n  702│        mut self,\n  703│        spawn_env: std::collections::BTreeMap<String, String>,\n     │...\n  733│    /// env_remove, and the recursion guard. Shared by both spawn paths.\n *734│    fn command_for(\n  735│        &self,\n  736│        role: &SessionRole,\n  737│        keys: &BTreeMap<String, String>,\n     │...\n  777│        }\n *778│        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): set the captured\n  779│        // read-env stamps as PROCESS ENV so a bare-env daemon spawn inherits the\n  780│        // parent's account root (e.g. a relocated CLAUDE_CONFIG_DIR) instead of the\n  781│        // default. AFTER env_remove (a stamped var is never stripped) and BEFORE the\n  782│        // recursion guard. A stamped var OVERRIDES the ambient value. Empty ⇒ no-op.\n *783│        // [impl->REQ-PSYCHE-SPAWN-ENV-PARITY]\n  784│        for (var, val) in &self.spawn_env {\n  785│            cmd.env(var, val);\n  786│        }\n     │...\n  802│        // the console-less daemon (notif pump) and the detached live-agent\n *803│        // host (psyche/echo/turn): without CREATE_NO_WINDOW each spawn flashes\n  804│        // a visible blank conhost on the desktop. Setting it here — the one\n  805│        // shared builder — makes KNOWN-HAZARDS 5.8 hold for every ManifestRuntime\n  806│        // spawn, not just the free run_bounded_command leg.\n     │...\n  836│        let role_def = self.role(role)?;\n *837│        let mut cmd = self.command_for(role_def, keys)?;\n  838│        // Discard stdio (the daemon/host is console-less; command_for already set\n  839│        // CREATE_NO_WINDOW on Windows) but RETAIN the child handle so a hosted caller\n  840│        // can reap it BY HANDLE (recycle-proof), unlike the detached spawn above.\n     │...\n  843│            .stderr(Stdio::null());\n *844│        let child = cmd.spawn().map_err(RuntimeError::Spawn)?;\n  845│        Ok(OwnedSession {\n  846│            pid: child.id(),\n  847│            child,\n     │...\n  858│        let role_def = self.role(role)?;\n *859│        let mut cmd = self.command_for(role_def, keys)?;\n  860│        cmd.stdin(if stdin.is_some() {\n  861│            Stdio::piped()\n  862│        } else {\n     │...\n  867│        let label = fill_template(&role_def.command, keys)?;\n *868│        let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;\n  869│        if let Some(bytes) = stdin {\n  870│            if let Some(mut sink) = child.stdin.take() {\n  871│                let owned = bytes.to_vec();\n     │...\n 1034│    }\n*1035│    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;\n 1036│    if let Some(bytes) = stdin {\n 1037│        if let Some(mut sink) = child.stdin.take() {\n 1038│            let owned = bytes.to_vec();\n     │...\n 1173│        let cmd = rt\n*1174│            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1175│            .unwrap();\n 1176│        assert_eq!(cmd.get_current_dir(), None, \"no default → inherit\");\n 1177│\n     │...\n 1180│        let cmd = rt\n*1181│            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1182│            .unwrap();\n 1183│        assert_eq!(\n 1184│            cmd.get_current_dir(),\n     │...\n 1198│        let cmd = rt\n*1199│            .command_for(rt.role(\"echo_commune\").unwrap(), &BTreeMap::new())\n 1200│            .unwrap();\n 1201│        assert_eq!(\n 1202│            cmd.get_current_dir().map(|p| p.to_string_lossy().replace('\\\\', \"/\")),\n     │...\n 1207│\n*1208│    // ---- REQ-PSYCHE-SPAWN-ENV-PARITY: read-env stamps threaded to the spawn ENV ----\n 1209│\n 1210│    /// The EXPLICITLY-set env vars on a built command (Command::get_envs yields\n 1211│    /// `Some` for a set var, `None` for an env_remove'd one) — the F-027 Half-B\n     │...\n 1234│\n*1235│    // [unit->REQ-PSYCHE-SPAWN-ENV-PARITY] captured read-env stamps threaded via\n 1236│    // `with_spawn_env` are applied as PROCESS ENV on the built psyche_resume command;\n 1237│    // a runtime with NO stamps sets no such env (the current non-psyche callers).\n 1238│    #[test]\n     │...\n 1243│        let role = plain.role(\"psyche_resume\").unwrap();\n*1244│        let cmd = plain.command_for(role, &BTreeMap::new()).unwrap();\n 1245│        assert!(\n 1246│            !env_set(&cmd).contains_key(\"CLAUDE_CONFIG_DIR\"),\n 1247│            \"no stamp → no env var set on the spawn\""},"isError":false,"timestamp":1784196001463}}
{"type":"message","id":"bd718a17","parentId":"ccf730fc","timestamp":"2026-07-16T10:00:01.468Z","message":{"role":"toolResult","toolCallId":"call_emxzNk3zyIoEN2mZkBMo1mN2|fc_0698befac979932d016a58aba108c4819a9345ea7de02d775a","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### dispatch.rs\n 37|//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n*38|//! | anything else | unknown — dropped, loud |\n 39|//!\n 40|//! A family whose production wiring lands in a later slice is still\n 41|//! classified here (the table is complete) but routed to a typed\n...\n 45|//!\n*46|//! Each claimed stream gets its own worker thread with its **own** brain IPC\n 47|//! connection (the broker is multi-client by design; the probe-brain pattern\n 48|//! every E2E test uses). The peek and the serve use **separate** connections:\n 49|//! the peek brain subscribes from 0 to read the first line and is dropped;\n...\n 115|    /// Unclassifiable — dropped.\n*116|    Unknown,\n 117|}\n 118|\n 119|/// Classify one decoded first line by shape (module-docs demux table).\n...\n 136|        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n*137|        Some(_) => StreamFamily::Unknown,\n 138|        None => {\n 139|            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\n 140|                StreamFamily::WanMsg\n...\n 151|            } else {\n*152|                StreamFamily::Unknown\n 153|            }\n 154|        }\n 155|    }\n...\n 163|    /// The first line never arrived / didn't parse — dropped.\n*164|    Unknown,\n 165|    /// The worker failed (IO against broker or store).\n 166|    Failed(String),\n 167|}\n...\n 205|    let mut brain = connect(broker_name)?;\n*206|    let mut claimed: HashSet<u64> = HashSet::new();\n 207|\n 208|    while !stop.load(Ordering::Relaxed) {\n 209|        let reply = brain.net_streams()?;\n...\n 213|            // next_seq guard also skips a row whose pump hasn't appended yet).\n*214|            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n 215|                continue;\n 216|            }\n 217|            let name = broker_name.to_string();\n...\n 282|\n*283|/// One claimed stream's worker: peek → classify → run the family's serve\n 284|/// loop on a fresh brain connection.\n 285|fn worker(\n 286|    broker_name: &str,\n...\n 293|        Ok(Some(v)) => v,\n*294|        Ok(None) => return DispatchOutcome::Unknown,\n*295|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 296|    };\n 297|    let family = classify_first_line(&line);\n 298|\n...\n 300|        Ok(b) => b,\n*301|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 302|    };\n 303|\n 304|    match family {\n...\n 307|                Ok(cs) => cs,\n*308|                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 309|            };\n 310|            let policy = SyncPolicy::load();\n 311|            match serve_sync(\n...\n 320|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*321|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 322|            }\n 323|        }\n 324|        StreamFamily::Update => {\n...\n 328|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*329|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 330|            }\n 331|        }\n 332|        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n...\n 337|            // from the broker's table (the spawn's label) — never from wire\n*338|            // bytes. An unlabeled or unknown session refuses, fail-closed.\n 339|            //\n 340|            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n 341|            // cannot know our local session id, so it names the ENDPOINT. When the\n...\n 387|                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*388|                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n 389|                },\n 390|                None => {\n 391|                    // Refuse politely: finish our send side, claim stays.\n 392|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n*393|                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394|                }\n 395|            }\n 396|        }\n...\n 412|                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*413|                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n 414|                    }\n 415|                }\n 416|                None => {\n...\n 430|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*431|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 432|            }\n 433|        }\n 434|        StreamFamily::ShellLink => {\n\n## tests/\n### dispatch.rs\n 344|// stream as locally-initiated; B's shows the same stream as peer-initiated\n*345|// (what the dispatcher claimed it by).\n 346|#[test]\n 347|fn dispatcher_funnels_wan_messages_undriven() {\n 348|    init_home();\n...\n 1012|    assert!(\n*1013|        !request_subnet_probe(&mut a, conn.conn_id, \"NoSuchNet\").expect(\"probe unknown\"),\n 1014|        \"a non-member subnet is not served\"\n 1015|    );\n 1016|\n...\n 1040|// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n*1041|// family (same serve arm + gate) rather than dropping as Unknown. An instance\n*1042|// row still classifies Registry; a bare unrelated object stays Unknown.\n 1043|#[test]\n 1044|fn classify_routes_a_node_label_feed_to_registry() {\n 1045|    use spt_daemon::dispatch::{classify_first_line, StreamFamily};\n...\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 1094|#[test]\n*1095|fn dispatcher_survives_an_unknown_stream() {\n 1096|    init_home();\n 1097|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 1098|    let dir = TempDir::new().unwrap();\n...\n 1152|        found,\n*1153|        \"the dispatcher survived the unknown stream and served the next one\"\n 1154|    );\n 1155|    stop.store(true, Ordering::Relaxed);\n 1156|}\n### attach.rs\n 1208|// controller is now a NON-BLOCKING try_send that DROPS on a full channel\n*1209|// (CONTROLLER_CHANNEL_DEPTH=4096), no longer a blocking writer. Under the 200KB\n 1210|// echo flood the controller falls behind its OWN output and the broker drops\n 1211|// controller frames (advancing nothing — `delivered_through` freezes), which\n 1212|// surfaces to the strict controller cursor as an `output gap`. The helper\n...\n 1218|// is deliberately oversized so we assert non-stall robustly, not a fragile\n*1219|// ring-fit burst size). Three regimes (CONTROLLER_CHANNEL_DEPTH=4096, ring=4096):\n*1220|// (1) <4096 frames fit → no drop → renders ISOLATED; (2) 4096..8192 → drop → gap →\n 1221|// re-fetch, ring still retains → RECOVERABLE → renders ISOLATED; (3) >8192 (this\n*1222|// 200KB burst) → the ring rolls PAST the frozen delivered_through → the dropped\n 1223|// frames are GONE → exactly-once is genuinely impossible → a clean\n 1224|// `ControllerIrrecoverablyBehind` (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND). The\n 1225|// RECOVERABLE re-fetch (regime 2) is proven deterministically by the in-process"}],"details":{"scopePath":"crates/spt-daemon/src/dispatch.rs, crates/spt-daemon/tests/dispatch.rs, crates/spt-daemon/tests/attach.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":31,"fileCount":3,"files":["crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/attach.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/dispatch.rs","count":20},{"path":"crates/spt-daemon/tests/dispatch.rs","count":7},{"path":"crates/spt-daemon/tests/attach.rs","count":4}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n\n## src/\n### dispatch.rs\n  37│//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n *38│//! | anything else | unknown — dropped, loud |\n  39│//!\n  40│//! A family whose production wiring lands in a later slice is still\n  41│//! classified here (the table is complete) but routed to a typed\n    │...\n  45│//!\n *46│//! Each claimed stream gets its own worker thread with its **own** brain IPC\n  47│//! connection (the broker is multi-client by design; the probe-brain pattern\n  48│//! every E2E test uses). The peek and the serve use **separate** connections:\n  49│//! the peek brain subscribes from 0 to read the first line and is dropped;\n    │...\n 115│    /// Unclassifiable — dropped.\n*116│    Unknown,\n 117│}\n 118│\n 119│/// Classify one decoded first line by shape (module-docs demux table).\n    │...\n 136│        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n*137│        Some(_) => StreamFamily::Unknown,\n 138│        None => {\n 139│            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\n 140│                StreamFamily::WanMsg\n    │...\n 151│            } else {\n*152│                StreamFamily::Unknown\n 153│            }\n 154│        }\n 155│    }\n    │...\n 163│    /// The first line never arrived / didn't parse — dropped.\n*164│    Unknown,\n 165│    /// The worker failed (IO against broker or store).\n 166│    Failed(String),\n 167│}\n    │...\n 205│    let mut brain = connect(broker_name)?;\n*206│    let mut claimed: HashSet<u64> = HashSet::new();\n 207│\n 208│    while !stop.load(Ordering::Relaxed) {\n 209│        let reply = brain.net_streams()?;\n    │...\n 213│            // next_seq guard also skips a row whose pump hasn't appended yet).\n*214│            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n 215│                continue;\n 216│            }\n 217│            let name = broker_name.to_string();\n    │...\n 282│\n*283│/// One claimed stream's worker: peek → classify → run the family's serve\n 284│/// loop on a fresh brain connection.\n 285│fn worker(\n 286│    broker_name: &str,\n    │...\n 293│        Ok(Some(v)) => v,\n*294│        Ok(None) => return DispatchOutcome::Unknown,\n*295│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 296│    };\n 297│    let family = classify_first_line(&line);\n 298│\n    │...\n 300│        Ok(b) => b,\n*301│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 302│    };\n 303│\n 304│    match family {\n    │...\n 307│                Ok(cs) => cs,\n*308│                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 309│            };\n 310│            let policy = SyncPolicy::load();\n 311│            match serve_sync(\n    │...\n 320│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*321│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 322│            }\n 323│        }\n 324│        StreamFamily::Update => {\n    │...\n 328│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*329│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 330│            }\n 331│        }\n 332│        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n    │...\n 337│            // from the broker's table (the spawn's label) — never from wire\n*338│            // bytes. An unlabeled or unknown session refuses, fail-closed.\n 339│            //\n 340│            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n 341│            // cannot know our local session id, so it names the ENDPOINT. When the\n    │...\n 387│                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*388│                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n 389│                },\n 390│                None => {\n 391│                    // Refuse politely: finish our send side, claim stays.\n 392│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n*393│                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394│                }\n 395│            }\n 396│        }\n    │...\n 412│                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*413│                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n 414│                    }\n 415│                }\n 416│                None => {\n    │...\n 430│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*431│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 432│            }\n 433│        }\n 434│        StreamFamily::ShellLink => {\n\n## tests/\n### dispatch.rs\n  344│// stream as locally-initiated; B's shows the same stream as peer-initiated\n *345│// (what the dispatcher claimed it by).\n  346│#[test]\n  347│fn dispatcher_funnels_wan_messages_undriven() {\n  348│    init_home();\n     │...\n 1012│    assert!(\n*1013│        !request_subnet_probe(&mut a, conn.conn_id, \"NoSuchNet\").expect(\"probe unknown\"),\n 1014│        \"a non-member subnet is not served\"\n 1015│    );\n 1016│\n     │...\n 1040│// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n*1041│// family (same serve arm + gate) rather than dropping as Unknown. An instance\n*1042│// row still classifies Registry; a bare unrelated object stays Unknown.\n 1043│#[test]\n 1044│fn classify_routes_a_node_label_feed_to_registry() {\n 1045│    use spt_daemon::dispatch::{classify_first_line, StreamFamily};\n     │...\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 1094│#[test]\n*1095│fn dispatcher_survives_an_unknown_stream() {\n 1096│    init_home();\n 1097│    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 1098│    let dir = TempDir::new().unwrap();\n     │...\n 1152│        found,\n*1153│        \"the dispatcher survived the unknown stream and served the next one\"\n 1154│    );\n 1155│    stop.store(true, Ordering::Relaxed);\n 1156│}\n### attach.rs\n 1208│// controller is now a NON-BLOCKING try_send that DROPS on a full channel\n*1209│// (CONTROLLER_CHANNEL_DEPTH=4096), no longer a blocking writer. Under the 200KB\n 1210│// echo flood the controller falls behind its OWN output and the broker drops\n 1211│// controller frames (advancing nothing — `delivered_through` freezes), which\n 1212│// surfaces to the strict controller cursor as an `output gap`. The helper\n     │...\n 1218│// is deliberately oversized so we assert non-stall robustly, not a fragile\n*1219│// ring-fit burst size). Three regimes (CONTROLLER_CHANNEL_DEPTH=4096, ring=4096):\n*1220│// (1) <4096 frames fit → no drop → renders ISOLATED; (2) 4096..8192 → drop → gap →\n 1221│// re-fetch, ring still retains → RECOVERABLE → renders ISOLATED; (3) >8192 (this\n*1222│// 200KB burst) → the ring rolls PAST the frozen delivered_through → the dropped\n 1223│// frames are GONE → exactly-once is genuinely impossible → a clean\n 1224│// `ControllerIrrecoverablyBehind` (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND). The\n 1225│// RECOVERABLE re-fetch (regime 2) is proven deterministically by the in-process"},"isError":false,"timestamp":1784196001468}}
{"type":"message","id":"a4f8ddd7","parentId":"bd718a17","timestamp":"2026-07-16T10:00:22.243Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying stream gap error on replay start**\n\n**Analyzing handle inheritance and update flow**"},{"type":"toolCall","id":"call_6K2PMNXrnztgvsrjKoviUT64|fc_0698befac979932d016a58abb57770819aa2b934a8624afe77","name":"grep","arguments":{"i":"Tracing fetch apply handoff","pattern":"fn cmd_update_fetch|cmd_update_apply\\(false\\)|if apply|ApplyMode|fetch_and_stage","path":"crates/spt/src/cli.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_tMZe8IrmI2nsYOE4q1tzLmwL|fc_0698befac979932d016a58abb5777c819abdc78dcea96273c7","name":"read","arguments":{"i":"Reading update fetch application tail","path":"crates/spt/src/cli.rs","selector":"6200-6350"}},{"type":"toolCall","id":"call_7HU9tjroholSLvO4Gbsa9Zc8|fc_0698befac979932d016a58abb57784819abfe6ce544d54d837","name":"read","arguments":{"i":"Reading update CLI definitions","path":"crates/spt/src/cli.rs","selector":"220-270,980-1035"}},{"type":"toolCall","id":"call_3wkp6dmH02pgRy9xAYTYRoAf|fc_0698befac979932d016a58abb5778c819a8f5933ffd4327e63","name":"grep","arguments":{"i":"Finding service manager update touch","pattern":"start_daemon\\(|stop_daemon\\(|ensure_running\\(|service|systemctl|schtasks","path":"crates/spt/src/cli.rs:4700-5250;crates/spt-daemon/src/applyhost.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_TxNrsVgEMigwb9U0uH0q73Kw|fc_0698befac979932d016a58abb577a4819a9d90c2021d2c1951","name":"read","arguments":{"i":"Reading service manager exact paths","path":"crates/spt-daemon/src/service.rs","selector":"1-115,130-190,195-275"}},{"type":"toolCall","id":"call_SBVR9lquCbKlgcFq5sEne120|fc_0698befac979932d016a58abb577ac819a9e2f3db5ccee1785","name":"read","arguments":{"i":"Reading dispatcher production test gaps","path":"crates/spt-daemon/tests/dispatch.rs","selector":"1080-1165"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":17648,"output":712,"cacheRead":170496,"cacheWrite":0,"totalTokens":188856,"reasoningTokens":376,"cost":{"input":0.08824000000000001,"output":0.02136,"cacheRead":0.08524799999999999,"cacheWrite":0,"total":0.19484800000000002}},"stopReason":"toolUse","timestamp":1784196001513,"responseId":"resp_0698befac979932d016a58aba1d55c819abdae25e327460339","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58aba43784819a8e260a1f1f842166","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKu1mBvBzFNdCylVxT0xjalYOlRDepjWLKNlRMJb1TTZNG44G076tZXyHQeI7CeAN0vwGV312lP-Zim4kSK3KdYs7PXK6hoUPQO49xJgYhesLP-DaUt9kgrK3uMb8poV6VdyjPA2qKiXkcBLJRzFfrQX1Nyzoulmza1m2QqAXgzOFrgkxUoVPqH27L_6XKNoiKqiJeU1Ry-Dqu3zSVNxtQ3tLx_aosDe3c5LWTZlaLHYF442Rzc5OQT0IRUYaOYBKrgdUemzDe1EgU7vIHbH2WRf9S_7z7c06I3ZFQI9dLhqde1lIUNk9ncwvQXWiKcvZUo_TP8M3yXVYC8ocxYH8FSF1yVXOqupHvPmom7EPidZfcoIJr9BLAdHxOourCdLMPI4XIyQE7XHi326f7E44JgFLSTKfa9rZ6A1o2rciwSVzE1c5umkwbVR5M5N5tmEu770bTqn-98IxfLfEqk_lMZsr17OgLXMsXCM5AEB1qK0aEDfMn1btdE13pZitCSCw0P100tA2g0PCx-YPtXgvz-wh83ov6tHfiF5AhO2UM5M3582lsqqQRxOxRsY64IErkh_XOHW9EIGTVdod5U9gvoZI3nYJTQvnYyOYY0BbZ-ZNssWyEqd0FfXxwWsMNcdisPb1X1ZGUqj0QIen6BJP8HUGeQItzuXBSkJt8hXFK7MEknV9_7ZUzwJzg72-L8955eba6L4BSaNXKCZUDwQR_23riYgHp8Pgfo4-5qV7NVdV5YBsI9Z5wYtKtBrTfHqXFcC0N2PDxwqvRxY-oXW7f1AH6_3T5AA_LTciaHxzOjhCTMmKkk0UfTaJJCzzEW6Ihl2fKO6LdeXhwbF3vK22C3BLbx6fB8oN51k1khDjG8OhPcMTBbK3KEs5tzFLHIW7SO3aA4gWIb7wUNQN9NeMwFH5PtUUG0hSzwFiw5EDdjMd1pX686F3o3Y-HztFDM_oGBSYpS4GC4_NqSM5IrgO8TTtQPPuIVqfRsdOmNPkjiUK6y3WXRbtsixzUSXpszY-ztCS2T8Rk94RObhZx9mpmrhPVNDO7XfwJkf2c56AodiiWlBtGCtip4b_HFrxll8Kwf80vxxj3cNH94MmSLAHPkwXQGQ5cf0NO3dkoEYf2CmZzErsToz-PDz1J3MpoDeaMBpwi7dTHU8fMkdwYVN9Y7DoonGUNk8TUrI7c5J65q3r1Xw6KWEXpQjBzwkFnUhRjC_k1x6anG2sPlZq7KZeFtv0sFwyqgZABA82LpTkUxfqeQhTTo0ZDEhpovianfip692bvFYoBWYb_Oi6jGVKXW0wfeWgZPLfy2rG-HlxlDE8KcHOIb5Nj9yP45YcAlymLBQIoagGshH3XYN6UN5tEO2sR6PKplTwN5MkRrk0J8TPMmPacJUcRXnSFextGfDCFkb8GU_FnGI77ptRcp5jlLDVuy5pg4cJdKnZozgKKYvFT7hSkCNQeyJ2zlEhzMf-PUMt5GU27Va0MCqm0ohBNUNVmImuvow8haiSITA7lILIB4T3E0dcqNJ_vT9ZwZLvneePycw0XKjBnoSNVr73pD5SorbdWuTq4TNe2JxDtP-E77XDn_l4I2Q-3d04tmzsMu6SAVYvfRmWB61hRpnJsVUmWCGIIhGrYZzuJddewakd8teS3ZjPlM2uJAtkZZI5m3crGNWBEUZxceoaNgiwIeiUwFB7p_I_O17cfoEsdGwcbxXnknlsa7Au0-L3dn8JLxehQO4Jx88R0BdCXgSewJy0zTdP27Utc62SO4yVUwZXWBBaVoQKPIZPjUcOkiFl4oHVXX3eaGfgldvdwZ4n7QmCecW4RdLOqcu29bBc08t54RYgm1DKhbjCsuffvkGjl3UhpJvexplUrqMg5gkGJkAkEp4La8J1aW9VW6lqlNYoTHij3IXBQBRhcBIJl7juah7BiBGbZMzXqXasMJ4NCEXmB2fSQ8wYe-KtZS09fGPBKu17NQrY0msd7MGdsCXLtaeqGTbc8gl5BgGLEDJMf3p0jEv0CqaGCEgYyapkF4y6Ge-wRGcgPUqu1uhKzhd0dsUHUxZNRsU2tSu5BqFiSWZjRdg_pyYGbP2TPb00qvYs3aHlGaoARUVoNmmmOAOlB7C0SvqHjKWpQ5zaeqKrLskJpC9EecbU1pojVchS0-c4dWdq68LA0DbcT9ctWp36nGnSuWmtu9Vmf010F2mhXjnoTINmQKcKp6bg8_plXwdDRdQB_H_i3bNVNJQlVmxwc1CTcWzqo348ll890hmt8NHvIURbLcFGyO8aeNVB0jFSAxHoSdjBXKvKjqvE8jj4TOQ-yIfm-ISy8kBqGQCuIFK6p_OYS076Sjp530Lf67EAh5-6Wf0-Gya-DYA0P0OavANVHvVmTTuNopWOPt2HsfEU2o7mYGQAw0N86xIhwMWkv-CrEcR4ltuvuru6lRvrNihn-UuN4-K4ls2mRepyUQAnKPEFL3nFreZmJGftioyQtvV56hGEXGYG92ksv3seJAan88iFK1YXVErXqIuwGt4TiHcqhXC4upqr5NAUQZjEdNfJ9NwjZiR3KZFGBwisP2cOj1jYyoh7cgj9jD9ARqLUmEi3lJsYqeBILstYRAJpEkTIjQc7qcBYV8Pym6YE8qFA2n42GV8XNODQpjP-nSI-6WwS5La4KKScHrBWOJh8uqk3RjA3WsmKF5r6V3_N7j6OH_FkQCu3-IpUFeTDpp-8N06FQv7GXd11i3YFmJXb22q3fl0H9beh62Ppzxqd32JijU-fQdi6JxpDu30vvKL2yFV5bVJ8TBIoW6naKII4liOWcHPmnsFqccMYXBFItzk7WP7OaRBCMsG3zvXZPe4Wb0K70r4WQvU1YHNGdUiFbh2kaoLKMt1H95OaKeCIjYpRK5PJvdxuLQfQZ4jxtR26IaX50lt2YL7o4fIn06tzGr9lpfVVzlNRHMZvB2VemUUkiqxfOuzkCFtU7mANRkksqeaKLfV5fmgOjQwTJ9Jrn0Tpx4m2qh7FrgN81U55DK7Vg39cixrz4LIUcfbtd3rp4t_RIgm-Ph0848m09A_GNhBvvmtAaqxMjK6KkS_E6T9WRMjz6lPDGW8sClTEiQEgAAAjVYpBSsA0hpy6kUnRPMHMf0yOFHPbWnfao16vAzCrj_njmdsLz-4OvhLCkNwwlhsE8nKtDGeDqN-0kgfi4J6pYaIUr3B0t7Lv7DuE_ZbLZ-SauMs5DgS19SK7BHQYQJocX3KNC1uJpiNywLbG6hs3WEFYm_WKeXngiT_zppHB1a5tn8v9xgxvnn2FMapFBdR63G22Xvcnzcc9qr7t0iHsgZF6i6UatlKZvkeA1CF","summary":[{"type":"summary_text","text":"**Identifying stream gap error on replay start**"},{"type":"summary_text","text":"**Analyzing handle inheritance and update flow**"}]},{"id":"fc_0698befac979932d016a58abb57770819aa2b934a8624afe77","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing fetch apply handoff\",\"pattern\":\"fn cmd_update_fetch|cmd_update_apply\\\\(false\\\\)|if apply|ApplyMode|fetch_and_stage\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_6K2PMNXrnztgvsrjKoviUT64","name":"grep"},{"id":"fc_0698befac979932d016a58abb5777c819abdc78dcea96273c7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update fetch application tail\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"6200-6350\"}","call_id":"call_tMZe8IrmI2nsYOE4q1tzLmwL","name":"read"},{"id":"fc_0698befac979932d016a58abb57784819abfe6ce544d54d837","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update CLI definitions\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"220-270,980-1035\"}","call_id":"call_7HU9tjroholSLvO4Gbsa9Zc8","name":"read"},{"id":"fc_0698befac979932d016a58abb5778c819a8f5933ffd4327e63","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding service manager update touch\",\"pattern\":\"start_daemon\\\\(|stop_daemon\\\\(|ensure_running\\\\(|service|systemctl|schtasks\",\"path\":\"crates/spt/src/cli.rs:4700-5250;crates/spt-daemon/src/applyhost.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_3wkp6dmH02pgRy9xAYTYRoAf","name":"grep"},{"id":"fc_0698befac979932d016a58abb577a4819a9d90c2021d2c1951","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading service manager exact paths\",\"path\":\"crates/spt-daemon/src/service.rs\",\"selector\":\"1-115,130-190,195-275\"}","call_id":"call_TxNrsVgEMigwb9U0uH0q73Kw","name":"read"},{"id":"fc_0698befac979932d016a58abb577ac819a9e2f3db5ccee1785","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher production test gaps\",\"path\":\"crates/spt-daemon/tests/dispatch.rs\",\"selector\":\"1080-1165\"}","call_id":"call_SBVR9lquCbKlgcFq5sEne120","name":"read"}]},"duration":20728.95679999981,"ttft":2684.7319999998435,"contextSnapshot":{"promptTokens":188144,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6K2PMNXrnztgvsrjKoviUT64|fc_0698befac979932d016a58abb57770819aa2b934a8624afe77","toolName":"grep","startedAt":"2026-07-16T10:00:22.243Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Tracing fetch apply handoff"},"id":"9ffe7b43","parentId":"a4f8ddd7","timestamp":"2026-07-16T10:00:22.243Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_tMZe8IrmI2nsYOE4q1tzLmwL|fc_0698befac979932d016a58abb5777c819abdc78dcea96273c7","toolName":"read","startedAt":"2026-07-16T10:00:22.243Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading update fetch application tail"},"id":"5b44a4eb","parentId":"9ffe7b43","timestamp":"2026-07-16T10:00:22.243Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7HU9tjroholSLvO4Gbsa9Zc8|fc_0698befac979932d016a58abb57784819abfe6ce544d54d837","toolName":"read","startedAt":"2026-07-16T10:00:22.243Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading update CLI definitions"},"id":"0a439053","parentId":"5b44a4eb","timestamp":"2026-07-16T10:00:22.243Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3wkp6dmH02pgRy9xAYTYRoAf|fc_0698befac979932d016a58abb5778c819a8f5933ffd4327e63","toolName":"grep","startedAt":"2026-07-16T10:00:22.244Z","args":{"path":"crates/spt/src/cli.rs:4700-5250;crates/spt-daemon/src/applyhost.rs"},"intent":"Finding service manager update touch"},"id":"5aab13c8","parentId":"0a439053","timestamp":"2026-07-16T10:00:22.244Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TxNrsVgEMigwb9U0uH0q73Kw|fc_0698befac979932d016a58abb577a4819a9d90c2021d2c1951","toolName":"read","startedAt":"2026-07-16T10:00:22.244Z","args":{"path":"crates/spt-daemon/src/service.rs"},"intent":"Reading service manager exact paths"},"id":"ef9ed260","parentId":"5aab13c8","timestamp":"2026-07-16T10:00:22.244Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_SBVR9lquCbKlgcFq5sEne120|fc_0698befac979932d016a58abb577ac819a9e2f3db5ccee1785","toolName":"read","startedAt":"2026-07-16T10:00:22.244Z","args":{"path":"crates/spt-daemon/tests/dispatch.rs"},"intent":"Reading dispatcher production test gaps"},"id":"4acaa541","parentId":"ef9ed260","timestamp":"2026-07-16T10:00:22.244Z"}
{"type":"message","id":"10aaa6e3","parentId":"4acaa541","timestamp":"2026-07-16T10:00:22.250Z","message":{"role":"toolResult","toolCallId":"call_6K2PMNXrnztgvsrjKoviUT64|fc_0698befac979932d016a58abb57770819aa2b934a8624afe77","toolName":"grep","content":[{"type":"text","text":" 6101|    match class {\n*6102|        FetchRejectClass::AlreadyStaged { .. } if apply => FetchAction::Apply,\n 6103|        FetchRejectClass::AlreadyStaged { .. } | FetchRejectClass::AlreadyApplied { .. } => {\n 6104|            FetchAction::DoneOk\n 6105|        }\n...\n 6109|\n*6110|fn cmd_update_fetch(channel: Option<String>, tag: Option<String>, apply: bool) -> i32 {\n 6111|    let repo = install_repo();\n 6112|\n 6113|    // The gh CLI is the mandated carrier (ADR-0036 §3): the channel is private,\n...\n 6201|            return match fetch_reject_action(class, apply) {\n*6202|                FetchAction::Apply => cmd_update_apply(false),\n 6203|                FetchAction::DoneOk => {\n 6204|                    match class {\n 6205|                        FetchRejectClass::AlreadyStaged { candidate } => println!(\n...\n 6329|            // auto-rollback.\n*6330|            if apply {\n*6331|                return cmd_update_apply(false);\n 6332|            }\n 6333|            0\n 6334|        }"}],"details":{"scopePath":"crates/spt/src/cli.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":5,"fileCount":1,"files":["crates/spt/src/cli.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":5}],"truncated":false,"displayContent":" 6101│    match class {\n*6102│        FetchRejectClass::AlreadyStaged { .. } if apply => FetchAction::Apply,\n 6103│        FetchRejectClass::AlreadyStaged { .. } | FetchRejectClass::AlreadyApplied { .. } => {\n 6104│            FetchAction::DoneOk\n 6105│        }\n     │...\n 6109│\n*6110│fn cmd_update_fetch(channel: Option<String>, tag: Option<String>, apply: bool) -> i32 {\n 6111│    let repo = install_repo();\n 6112│\n 6113│    // The gh CLI is the mandated carrier (ADR-0036 §3): the channel is private,\n     │...\n 6201│            return match fetch_reject_action(class, apply) {\n*6202│                FetchAction::Apply => cmd_update_apply(false),\n 6203│                FetchAction::DoneOk => {\n 6204│                    match class {\n 6205│                        FetchRejectClass::AlreadyStaged { candidate } => println!(\n     │...\n 6329│            // auto-rollback.\n*6330│            if apply {\n*6331│                return cmd_update_apply(false);\n 6332│            }\n 6333│            0\n 6334│        }"},"isError":false,"timestamp":1784196022250}}
{"type":"message","id":"b59a6660","parentId":"10aaa6e3","timestamp":"2026-07-16T10:00:22.391Z","message":{"role":"toolResult","toolCallId":"call_TxNrsVgEMigwb9U0uH0q73Kw|fc_0698befac979932d016a58abb577a4819a9d90c2021d2c1951","toolName":"read","content":[{"type":"text","text":"//! Platform daemon-service abstraction (REQ-DAEMON-6/-8).\n//!\n//! When an OS service manager has a registered spt-daemon for this user, the\n//! `daemon start`/`daemon stop` verbs and the internal `ensure_running`\n//! auto-start route THROUGH it — never spawning a competing manual daemon that\n//! would fight an auto-restarting unit for the broker socket (the kitsubito\n//! 2026-06-08 restart-loop, where a manual `spt daemon run` raced the systemd\n//! `spt-daemon` user service).\n//!\n//! **Detection source of truth (decision 2026-06-08).** Linux = the systemd\n//! user-unit FILE presence at the canonical XDG path. A cheap check (no\n//! subprocess) that is honest: the installer writes the unit there, so its\n//! presence means a unit is registered. The control ops (`start`/`stop`/\n//! `is-active`) necessarily shell out to `systemctl --user` anyway.\n//!\n//! **Windows = the at-logon scheduled task is boot-only**, NOT a start/stop-\n//! controllable service (DAEMON-LIFECYCLE decision b): `detected()` is false,\n//! so start stays a detached spawn and stop stays an IPC stop; the task is\n//! surfaced only as a `daemon status` boot hint.\n\nuse std::io;\n#[allow(unused_imports)]\nuse std::path::PathBuf;\n#[allow(unused_imports)]\nuse std::process::Command;\n\n/// What `daemon start` (and `ensure_running`) resolves to once the live +\n/// service state is known. Pure routing target so the decision is unit-tested\n/// without touching the OS.\n#[derive(Debug, PartialEq, Eq)]\npub enum StartPlan {\n    /// A daemon already answers — nothing to do (idempotent).\n    AlreadyRunning,\n    /// A manager owns a registered service — start via it (never a competing\n    /// manual spawn).\n    ServiceStart,\n    /// No manager — spawn a detached manual daemon (today's path).\n    ManualSpawn,\n}\n\n/// What `daemon stop` resolves to.\n#[derive(Debug, PartialEq, Eq)]\npub enum StopPlan {\n    /// Nothing is running — already stopped.\n    NotRunning,\n    /// A registered + active service — stop via its manager so it does not\n    /// auto-restart-fight the teardown.\n    ServiceStop,\n    /// A manual daemon is running — graceful IPC stop.\n    IpcStop,\n}\n\n/// Pure router for `start`/`ensure_running` (REQ-DAEMON-6/-8): prefer the\n/// manager when one is registered, never a competing manual spawn.\n// [impl->REQ-DAEMON-6]\n// [impl->REQ-DAEMON-8]\npub fn plan_start(running: bool, service_detected: bool) -> StartPlan {\n    if running {\n        StartPlan::AlreadyRunning\n    } else if service_detected {\n        StartPlan::ServiceStart\n    } else {\n        StartPlan::ManualSpawn\n    }\n}\n\n/// Pure router for `stop` (REQ-DAEMON-6): a registered + active service is\n/// stopped through its manager; otherwise a running manual daemon takes the\n/// IPC stop; nothing running is already stopped.\n// [impl->REQ-DAEMON-6]\npub fn plan_stop(running: bool, service_detected: bool, service_active: bool) -> StopPlan {\n    if service_detected && service_active {\n        StopPlan::ServiceStop\n    } else if running {\n        StopPlan::IpcStop\n    } else {\n        StopPlan::NotRunning\n    }\n}\n\n/// `daemon run` is foreground by contract (REQ-DAEMON-7). It refuses ONLY when\n/// it would otherwise vanish: on Windows, when an elevated invocation has a\n/// real de-elevation target (it would respawn detached/de-elevated, KH 5.7).\n/// A uniformly-elevated or headless Windows session has NO target — there `run`\n/// serves elevated in the foreground consistently (and must, e.g. an elevated\n/// self-hosted CI runner with no desktop shell). Unix always serves foreground\n/// (privileges drop in-process). So the refuse is gated on \"would vanish\", not\n/// on elevation alone.\n// [impl->REQ-DAEMON-7]\npub fn run_refused_when_vanishing(is_windows: bool, would_vanish: bool) -> bool {\n    is_windows && would_vanish\n}\n\n/// A platform service manager controlling the spt-daemon for this user.\npub trait DaemonService {\n    /// A controllable manager is registered — a cheap probe (file/registry\n    /// check, no subprocess). When true, start/stop route through it.\n    fn detected(&self) -> bool;\n    /// The service is currently active (the manager reports it running).\n    fn is_active(&self) -> bool;\n    fn start(&self) -> io::Result<()>;\n    fn stop(&self) -> io::Result<()>;\n    /// Short label for `daemon status` + conflict warnings (\"systemd --user\").\n    fn label(&self) -> &'static str;\n    /// A boot-autostart note for `daemon status`, when the platform has one\n    /// that isn't a start/stop-controllable service (the Windows logon task,\n    /// or the systemd unit's enable-state). `None` = nothing to add.\n    fn boot_hint(&self) -> Option<String> {\n        None\n    }\n}\n\n// ── Linux: systemd user unit ────────────────────────────────────────────────\n\n/// The canonical systemd user-unit path — the `install.sh` target\n…\nfn systemd_unit_path() -> Option<PathBuf> {\n    systemd_unit_path_from(\n        std::env::var(\"XDG_CONFIG_HOME\").ok().as_deref(),\n        std::env::var(\"HOME\").ok().as_deref(),\n    )\n}\n\n/// The installer's systemd unit runs with no `SPT_HOME` → it serves the\n/// DEFAULT home only. A `SPT_HOME`-overridden invocation (tests, a secondary\n/// home) is NOT the service's home, so the service must not be driven for it\n/// (else a custom-home command would shell out to `systemctl --user start\n/// spt-daemon` — which serves the wrong home, or fails in a bus-less context).\n/// Detection is gated on this so service routing only applies on the home the\n/// service actually manages.\n#[cfg(target_os = \"linux\")]\nfn manages_default_home() -> bool {\n    std::env::var_os(\"SPT_HOME\").is_none()\n}\n\n#[cfg(target_os = \"linux\")]\nfn run_systemctl(args: &[&str]) -> io::Result<()> {\n    let status = Command::new(\"systemctl\").args(args).status()?;\n    if status.success() {\n        Ok(())\n    } else {\n        Err(io::Error::other(format!(\n            \"systemctl {} failed ({status})\",\n            args.join(\" \")\n        )))\n    }\n}\n\n#[cfg(target_os = \"linux\")]\npub struct SystemdUserService;\n\n#[cfg(target_os = \"linux\")]\nimpl DaemonService for SystemdUserService {\n    fn detected(&self) -> bool {\n        manages_default_home() && systemd_unit_path().map(|p| p.exists()).unwrap_or(false)\n    }\n    fn is_active(&self) -> bool {\n        Command::new(\"systemctl\")\n            .args([\"--user\", \"is-active\", \"--quiet\", \"spt-daemon.service\"])\n            .status()\n            .map(|s| s.success())\n            .unwrap_or(false)\n    }\n    fn start(&self) -> io::Result<()> {\n        run_systemctl(&[\"--user\", \"start\", \"spt-daemon.service\"])\n    }\n    fn stop(&self) -> io::Result<()> {\n        run_systemctl(&[\"--user\", \"stop\", \"spt-daemon.service\"])\n    }\n    fn label(&self) -> &'static str {\n        \"systemd --user\"\n    }\n    fn boot_hint(&self) -> Option<String> {\n        let enabled = Command::new(\"systemctl\")\n            .args([\"--user\", \"is-enabled\", \"--quiet\", \"spt-daemon.service\"])\n            .status()\n            .map(|s| s.success())\n            .unwrap_or(false);\n        Some(if enabled {\n…\n        } else {\n…\n        })\n    }\n}\n\n// ── Windows: at-logon scheduled task (boot-only, NOT start/stop-controllable) ─\n\n#[cfg(windows)]\nfn logon_task_present() -> bool {\n    Command::new(\"schtasks\")\n        .args([\"/Query\", \"/TN\", \"spt-core daemon\"])\n        .stdout(std::process::Stdio::null())\n        .stderr(std::process::Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false)\n}\n\n/// Whether a registered at-logon action string is the STALE, unsafe BARE\n/// `daemon run` form (REQ-HAZARD-DETACHED-DAEMON-STDIO): a legacy-installer\n/// registration that launches the daemon detached-in-fact WITHOUT `--detached`\n/// (confirmed live field-drift on ENLYZEAM). The safe forms are `daemon start`\n/// (routes `spawn_detached` → `--detached`) or an explicit `daemon run --detached`.\n/// Pure so both CI legs unit-test the classification. // [impl->REQ-HAZARD-DETACHED-DAEMON-STDIO]\npub fn logon_task_action_is_stale_bare(action: &str) -> bool {\n    let a = action.to_lowercase();\n    a.contains(\"daemon run\") && !a.contains(\"--detached\") && !a.contains(\"daemon start\")\n}\n\n/// The at-logon task's registered action string (\"Task To Run\"), via\n/// `schtasks /Query /V`, or `None` when the task is absent/unreadable (or the\n/// label is localized past our parse — best-effort, the nag simply doesn't fire).\n#[cfg(windows)]\nfn logon_task_action() -> Option<String> {\n    let out = Command::new(\"schtasks\")\n        .args([\"/Query\", \"/TN\", \"spt-core daemon\", \"/V\", \"/FO\", \"LIST\"])\n        .output()\n        .ok()?;\n    if !out.status.success() {\n        return None;\n    }\n    let text = String::from_utf8_lossy(&out.stdout);\n    for line in text.lines() {\n        if let Some(rest) = line.trim().strip_prefix(\"Task To Run:\") {\n            return Some(rest.trim().to_string());\n        }\n    }\n    None\n}\n\n#[cfg(windows)]\npub struct WindowsLogonTask;\n\n#[cfg(windows)]\nimpl DaemonService for WindowsLogonTask {\n    // The logon task is a boot trigger, not a controllable service — so\n    // start/stop never route through it (decision b); `detected()` is false.\n    fn detected(&self) -> bool {\n        false\n    }\n    fn is_active(&self) -> bool {\n        false\n    }\n    fn start(&self) -> io::Result<()> {\n        Err(io::Error::other(\n            \"no controllable service on Windows (the logon task is boot-only)\",\n        ))\n    }\n    fn stop(&self) -> io::Result<()> {\n        Err(io::Error::other(\n            \"no controllable service on Windows (the logon task is boot-only)\",\n        ))\n    }\n    fn label(&self) -> &'static str {\n        \"manual\"\n    }\n    fn boot_hint(&self) -> Option<String> {\n        if !logon_task_present() {\n            return None;\n        }\n        let base = \"at-logon task 'spt-core daemon' registered (auto-starts at logon)\".to_string();\n        // Drift nag (REQ-HAZARD-DETACHED-DAEMON-STDIO): a legacy installer may have\n…\n    }\n}"}],"details":{"displayContent":{"text":"//! Platform daemon-service abstraction (REQ-DAEMON-6/-8).\n//!\n//! When an OS service manager has a registered spt-daemon for this user, the\n//! `daemon start`/`daemon stop` verbs and the internal `ensure_running`\n//! auto-start route THROUGH it — never spawning a competing manual daemon that\n//! would fight an auto-restarting unit for the broker socket (the kitsubito\n//! 2026-06-08 restart-loop, where a manual `spt daemon run` raced the systemd\n//! `spt-daemon` user service).\n//!\n//! **Detection source of truth (decision 2026-06-08).** Linux = the systemd\n//! user-unit FILE presence at the canonical XDG path. A cheap check (no\n//! subprocess) that is honest: the installer writes the unit there, so its\n//! presence means a unit is registered. The control ops (`start`/`stop`/\n//! `is-active`) necessarily shell out to `systemctl --user` anyway.\n//!\n//! **Windows = the at-logon scheduled task is boot-only**, NOT a start/stop-\n//! controllable service (DAEMON-LIFECYCLE decision b): `detected()` is false,\n//! so start stays a detached spawn and stop stays an IPC stop; the task is\n//! surfaced only as a `daemon status` boot hint.\n\nuse std::io;\n#[allow(unused_imports)]\nuse std::path::PathBuf;\n#[allow(unused_imports)]\nuse std::process::Command;\n\n/// What `daemon start` (and `ensure_running`) resolves to once the live +\n/// service state is known. Pure routing target so the decision is unit-tested\n/// without touching the OS.\n#[derive(Debug, PartialEq, Eq)]\npub enum StartPlan {\n    /// A daemon already answers — nothing to do (idempotent).\n    AlreadyRunning,\n    /// A manager owns a registered service — start via it (never a competing\n    /// manual spawn).\n    ServiceStart,\n    /// No manager — spawn a detached manual daemon (today's path).\n    ManualSpawn,\n}\n\n/// What `daemon stop` resolves to.\n#[derive(Debug, PartialEq, Eq)]\npub enum StopPlan {\n    /// Nothing is running — already stopped.\n    NotRunning,\n    /// A registered + active service — stop via its manager so it does not\n    /// auto-restart-fight the teardown.\n    ServiceStop,\n    /// A manual daemon is running — graceful IPC stop.\n    IpcStop,\n}\n\n/// Pure router for `start`/`ensure_running` (REQ-DAEMON-6/-8): prefer the\n/// manager when one is registered, never a competing manual spawn.\n// [impl->REQ-DAEMON-6]\n// [impl->REQ-DAEMON-8]\npub fn plan_start(running: bool, service_detected: bool) -> StartPlan {\n    if running {\n        StartPlan::AlreadyRunning\n    } else if service_detected {\n        StartPlan::ServiceStart\n    } else {\n        StartPlan::ManualSpawn\n    }\n}\n\n/// Pure router for `stop` (REQ-DAEMON-6): a registered + active service is\n/// stopped through its manager; otherwise a running manual daemon takes the\n/// IPC stop; nothing running is already stopped.\n// [impl->REQ-DAEMON-6]\npub fn plan_stop(running: bool, service_detected: bool, service_active: bool) -> StopPlan {\n    if service_detected && service_active {\n        StopPlan::ServiceStop\n    } else if running {\n        StopPlan::IpcStop\n    } else {\n        StopPlan::NotRunning\n    }\n}\n\n/// `daemon run` is foreground by contract (REQ-DAEMON-7). It refuses ONLY when\n/// it would otherwise vanish: on Windows, when an elevated invocation has a\n/// real de-elevation target (it would respawn detached/de-elevated, KH 5.7).\n/// A uniformly-elevated or headless Windows session has NO target — there `run`\n/// serves elevated in the foreground consistently (and must, e.g. an elevated\n/// self-hosted CI runner with no desktop shell). Unix always serves foreground\n/// (privileges drop in-process). So the refuse is gated on \"would vanish\", not\n/// on elevation alone.\n// [impl->REQ-DAEMON-7]\npub fn run_refused_when_vanishing(is_windows: bool, would_vanish: bool) -> bool {\n    is_windows && would_vanish\n}\n\n/// A platform service manager controlling the spt-daemon for this user.\npub trait DaemonService {\n    /// A controllable manager is registered — a cheap probe (file/registry\n    /// check, no subprocess). When true, start/stop route through it.\n    fn detected(&self) -> bool;\n    /// The service is currently active (the manager reports it running).\n    fn is_active(&self) -> bool;\n    fn start(&self) -> io::Result<()>;\n    fn stop(&self) -> io::Result<()>;\n    /// Short label for `daemon status` + conflict warnings (\"systemd --user\").\n    fn label(&self) -> &'static str;\n    /// A boot-autostart note for `daemon status`, when the platform has one\n    /// that isn't a start/stop-controllable service (the Windows logon task,\n    /// or the systemd unit's enable-state). `None` = nothing to add.\n    fn boot_hint(&self) -> Option<String> {\n        None\n    }\n}\n\n// ── Linux: systemd user unit ────────────────────────────────────────────────\n\n/// The canonical systemd user-unit path — the `install.sh` target\n…\nfn systemd_unit_path() -> Option<PathBuf> {\n    systemd_unit_path_from(\n        std::env::var(\"XDG_CONFIG_HOME\").ok().as_deref(),\n        std::env::var(\"HOME\").ok().as_deref(),\n    )\n}\n\n/// The installer's systemd unit runs with no `SPT_HOME` → it serves the\n/// DEFAULT home only. A `SPT_HOME`-overridden invocation (tests, a secondary\n/// home) is NOT the service's home, so the service must not be driven for it\n/// (else a custom-home command would shell out to `systemctl --user start\n/// spt-daemon` — which serves the wrong home, or fails in a bus-less context).\n/// Detection is gated on this so service routing only applies on the home the\n/// service actually manages.\n#[cfg(target_os = \"linux\")]\nfn manages_default_home() -> bool {\n    std::env::var_os(\"SPT_HOME\").is_none()\n}\n\n#[cfg(target_os = \"linux\")]\nfn run_systemctl(args: &[&str]) -> io::Result<()> {\n    let status = Command::new(\"systemctl\").args(args).status()?;\n    if status.success() {\n        Ok(())\n    } else {\n        Err(io::Error::other(format!(\n            \"systemctl {} failed ({status})\",\n            args.join(\" \")\n        )))\n    }\n}\n\n#[cfg(target_os = \"linux\")]\npub struct SystemdUserService;\n\n#[cfg(target_os = \"linux\")]\nimpl DaemonService for SystemdUserService {\n    fn detected(&self) -> bool {\n        manages_default_home() && systemd_unit_path().map(|p| p.exists()).unwrap_or(false)\n    }\n    fn is_active(&self) -> bool {\n        Command::new(\"systemctl\")\n            .args([\"--user\", \"is-active\", \"--quiet\", \"spt-daemon.service\"])\n            .status()\n            .map(|s| s.success())\n            .unwrap_or(false)\n    }\n    fn start(&self) -> io::Result<()> {\n        run_systemctl(&[\"--user\", \"start\", \"spt-daemon.service\"])\n    }\n    fn stop(&self) -> io::Result<()> {\n        run_systemctl(&[\"--user\", \"stop\", \"spt-daemon.service\"])\n    }\n    fn label(&self) -> &'static str {\n        \"systemd --user\"\n    }\n    fn boot_hint(&self) -> Option<String> {\n        let enabled = Command::new(\"systemctl\")\n            .args([\"--user\", \"is-enabled\", \"--quiet\", \"spt-daemon.service\"])\n            .status()\n            .map(|s| s.success())\n            .unwrap_or(false);\n        Some(if enabled {\n…\n        } else {\n…\n        })\n    }\n}\n\n// ── Windows: at-logon scheduled task (boot-only, NOT start/stop-controllable) ─\n\n#[cfg(windows)]\nfn logon_task_present() -> bool {\n    Command::new(\"schtasks\")\n        .args([\"/Query\", \"/TN\", \"spt-core daemon\"])\n        .stdout(std::process::Stdio::null())\n        .stderr(std::process::Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false)\n}\n\n/// Whether a registered at-logon action string is the STALE, unsafe BARE\n/// `daemon run` form (REQ-HAZARD-DETACHED-DAEMON-STDIO): a legacy-installer\n/// registration that launches the daemon detached-in-fact WITHOUT `--detached`\n/// (confirmed live field-drift on ENLYZEAM). The safe forms are `daemon start`\n/// (routes `spawn_detached` → `--detached`) or an explicit `daemon run --detached`.\n/// Pure so both CI legs unit-test the classification. // [impl->REQ-HAZARD-DETACHED-DAEMON-STDIO]\npub fn logon_task_action_is_stale_bare(action: &str) -> bool {\n    let a = action.to_lowercase();\n    a.contains(\"daemon run\") && !a.contains(\"--detached\") && !a.contains(\"daemon start\")\n}\n\n/// The at-logon task's registered action string (\"Task To Run\"), via\n/// `schtasks /Query /V`, or `None` when the task is absent/unreadable (or the\n/// label is localized past our parse — best-effort, the nag simply doesn't fire).\n#[cfg(windows)]\nfn logon_task_action() -> Option<String> {\n    let out = Command::new(\"schtasks\")\n        .args([\"/Query\", \"/TN\", \"spt-core daemon\", \"/V\", \"/FO\", \"LIST\"])\n        .output()\n        .ok()?;\n    if !out.status.success() {\n        return None;\n    }\n    let text = String::from_utf8_lossy(&out.stdout);\n    for line in text.lines() {\n        if let Some(rest) = line.trim().strip_prefix(\"Task To Run:\") {\n            return Some(rest.trim().to_string());\n        }\n    }\n    None\n}\n\n#[cfg(windows)]\npub struct WindowsLogonTask;\n\n#[cfg(windows)]\nimpl DaemonService for WindowsLogonTask {\n    // The logon task is a boot trigger, not a controllable service — so\n    // start/stop never route through it (decision b); `detected()` is false.\n    fn detected(&self) -> bool {\n        false\n    }\n    fn is_active(&self) -> bool {\n        false\n    }\n    fn start(&self) -> io::Result<()> {\n        Err(io::Error::other(\n            \"no controllable service on Windows (the logon task is boot-only)\",\n        ))\n    }\n    fn stop(&self) -> io::Result<()> {\n        Err(io::Error::other(\n            \"no controllable service on Windows (the logon task is boot-only)\",\n        ))\n    }\n    fn label(&self) -> &'static str {\n        \"manual\"\n    }\n    fn boot_hint(&self) -> Option<String> {\n        if !logon_task_present() {\n            return None;\n        }\n        let base = \"at-logon task 'spt-core daemon' registered (auto-starts at logon)\".to_string();\n        // Drift nag (REQ-HAZARD-DETACHED-DAEMON-STDIO): a legacy installer may have\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,null,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,null,193,null,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,null,287,288]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\service.rs"}}},"isError":false,"timestamp":1784196022390}}
{"type":"message","id":"0944e8e2","parentId":"b59a6660","timestamp":"2026-07-16T10:00:22.391Z","message":{"role":"toolResult","toolCallId":"call_7HU9tjroholSLvO4Gbsa9Zc8|fc_0698befac979932d016a58abb57784819abfe6ce544d54d837","toolName":"read","content":[{"type":"text","text":"    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing\n    /// an existing offline instance back is `relink` / `persistent` /\n    /// wake.\n    Shell {\n        #[command(subcommand)]\n        action: ShellCmd,\n    },\n    /// Inspect and acknowledge notifications.\n    ///\n    /// Dismissal is the explicit ack — it latches and replicates\n    /// subnet-wide.\n    Notif {\n        #[command(subcommand)]\n        action: NotifCmd,\n    },\n    /// Self-update: bare `spt update` brings the whole node current.\n    ///\n    /// The bare form fetches + installs the latest core release, then updates\n    /// every release-shipped adapter — one command. The invoking session\n    /// survives it: installing cycles only the daemon's coordinator process,\n    /// never the hosted terminals. `apply` is the explicit ack named by the\n    /// update-consent notification; it re-verifies the staged release before\n    /// touching the live daemon.\n    #[command(args_conflicts_with_subcommands = true)]\n    Update {\n        #[command(subcommand)]\n        action: Option<UpdateCmd>,\n        /// Update the core binary only — skip the adapters leg of the bare\n        /// composite.\n        #[arg(long, short = 'c')]\n        core_only: bool,\n        /// The full-cycle form: fetch, update adapters, then finish by\n        /// restarting the daemon onto the new version (`update apply --finish`)\n        /// as the final step — so the whole node, coordinator and live agents,\n        /// runs the new version when it returns. The restart bounces hosted\n        /// sessions (they come back automatically).\n        #[arg(long)]\n        restart: bool,\n    },\n    /// The node-local docs: open them in your browser, or print their URL.\n    ///\n    /// Every release ships a version-matched docs bundle; the daemon serves\n    /// it on loopback. Bare `spt docs` opens the browser; `spt docs url`\n    /// prints the resolved URL for tools and agents.\n    Docs {\n        #[command(subcommand)]\n        action: Option<DocsCmd>,\n    },\n    /// Self-install this binary onto the node (the bootstrap path).\n    ///\n    /// Run it from a downloaded release binary: it places itself at the\n…\nenum NotifCmd {\n…\n    List {\n…\n    },\n    /// Dismiss (ack) a notification by id — latches, replicates subnet-wide.\n    Dismiss {\n        /// The notif id (as shown by `spt notif list`).\n        notif_id: String,\n    },\n}\n\n/// `spt update <action>` subcommands (REQ-UPD-4).\n#[derive(Subcommand)]\nenum UpdateCmd {\n    /// Apply the staged, verified self-update now.\n    Apply {\n        /// Finish onto the new version in one step: install it, then restart the\n        /// daemon so both the coordinator and every live agent run the new\n        /// version. Hosted sessions come back automatically — no manual restart.\n        /// Without this flag, install alone leaves the running daemon on the\n        /// previous version until you restart it yourself.\n        #[arg(long)]\n        finish: bool,\n    },\n    /// Fetch the latest signed release from the GitHub origin and stage it\n    /// (then `spt update apply`). Bootstraps a node with no peer to pull from.\n    Fetch {\n        /// Accept a release on this channel instead of the node's pin (e.g.\n        /// `beta`). Default: the node's pinned channel.\n        #[arg(long)]\n        channel: Option<String>,\n        /// Fetch a specific release tag (e.g. `v0.3.1`) instead of the latest.\n        #[arg(long)]\n        tag: Option<String>,\n        /// Fetch then install in one step — apply the staged update even if the\n        /// latest was already downloaded. The one-shot \"get me to the latest\".\n        #[arg(long)]\n        apply: bool,\n    },\n    /// Update release-shipped adapters (an alias of `spt adapter update`,\n    /// which also stays). With no names, every release-shipped adapter is\n    /// swept; with names (comma-separated), exactly those. Names are\n    /// validated before anything updates, one adapter's failure never stops\n    /// the rest, and a summary line reports each outcome.\n    Adapters {\n        /// Adapters to update, comma-separated (e.g. `claude-spt,other`).\n        /// Omit to sweep every release-shipped adapter.\n        names: Option<String>,\n    },\n}\n\n/// `spt subnet <action>` subcommands (REQ-SUBNET-1). Noun namespace with\n/// subcommand verbs and positional subnet names — never `--subnet` flags\n/// (M7 plan decision 3).\n#[derive(Subcommand)]\nenum SubnetCmd {\n    /// Show subnet membership: name, paired nodes, endpoints.\n    ///\n    /// Never prints seeds, epochs, or pairing codes. Bare `spt subnet` is\n…\n}"}],"details":{"displayContent":{"text":"    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing\n    /// an existing offline instance back is `relink` / `persistent` /\n    /// wake.\n    Shell {\n        #[command(subcommand)]\n        action: ShellCmd,\n    },\n    /// Inspect and acknowledge notifications.\n    ///\n    /// Dismissal is the explicit ack — it latches and replicates\n    /// subnet-wide.\n    Notif {\n        #[command(subcommand)]\n        action: NotifCmd,\n    },\n    /// Self-update: bare `spt update` brings the whole node current.\n    ///\n    /// The bare form fetches + installs the latest core release, then updates\n    /// every release-shipped adapter — one command. The invoking session\n    /// survives it: installing cycles only the daemon's coordinator process,\n    /// never the hosted terminals. `apply` is the explicit ack named by the\n    /// update-consent notification; it re-verifies the staged release before\n    /// touching the live daemon.\n    #[command(args_conflicts_with_subcommands = true)]\n    Update {\n        #[command(subcommand)]\n        action: Option<UpdateCmd>,\n        /// Update the core binary only — skip the adapters leg of the bare\n        /// composite.\n        #[arg(long, short = 'c')]\n        core_only: bool,\n        /// The full-cycle form: fetch, update adapters, then finish by\n        /// restarting the daemon onto the new version (`update apply --finish`)\n        /// as the final step — so the whole node, coordinator and live agents,\n        /// runs the new version when it returns. The restart bounces hosted\n        /// sessions (they come back automatically).\n        #[arg(long)]\n        restart: bool,\n    },\n    /// The node-local docs: open them in your browser, or print their URL.\n    ///\n    /// Every release ships a version-matched docs bundle; the daemon serves\n    /// it on loopback. Bare `spt docs` opens the browser; `spt docs url`\n    /// prints the resolved URL for tools and agents.\n    Docs {\n        #[command(subcommand)]\n        action: Option<DocsCmd>,\n    },\n    /// Self-install this binary onto the node (the bootstrap path).\n    ///\n    /// Run it from a downloaded release binary: it places itself at the\n…\nenum NotifCmd {\n…\n    List {\n…\n    },\n    /// Dismiss (ack) a notification by id — latches, replicates subnet-wide.\n    Dismiss {\n        /// The notif id (as shown by `spt notif list`).\n        notif_id: String,\n    },\n}\n\n/// `spt update <action>` subcommands (REQ-UPD-4).\n#[derive(Subcommand)]\nenum UpdateCmd {\n    /// Apply the staged, verified self-update now.\n    Apply {\n        /// Finish onto the new version in one step: install it, then restart the\n        /// daemon so both the coordinator and every live agent run the new\n        /// version. Hosted sessions come back automatically — no manual restart.\n        /// Without this flag, install alone leaves the running daemon on the\n        /// previous version until you restart it yourself.\n        #[arg(long)]\n        finish: bool,\n    },\n    /// Fetch the latest signed release from the GitHub origin and stage it\n    /// (then `spt update apply`). Bootstraps a node with no peer to pull from.\n    Fetch {\n        /// Accept a release on this channel instead of the node's pin (e.g.\n        /// `beta`). Default: the node's pinned channel.\n        #[arg(long)]\n        channel: Option<String>,\n        /// Fetch a specific release tag (e.g. `v0.3.1`) instead of the latest.\n        #[arg(long)]\n        tag: Option<String>,\n        /// Fetch then install in one step — apply the staged update even if the\n        /// latest was already downloaded. The one-shot \"get me to the latest\".\n        #[arg(long)]\n        apply: bool,\n    },\n    /// Update release-shipped adapters (an alias of `spt adapter update`,\n    /// which also stays). With no names, every release-shipped adapter is\n    /// swept; with names (comma-separated), exactly those. Names are\n    /// validated before anything updates, one adapter's failure never stops\n    /// the rest, and a summary line reports each outcome.\n    Adapters {\n        /// Adapters to update, comma-separated (e.g. `claude-spt,other`).\n        /// Omit to sweep every release-shipped adapter.\n        names: Option<String>,\n    },\n}\n\n/// `spt subnet <action>` subcommands (REQ-SUBNET-1). Noun namespace with\n/// subcommand verbs and positional subnet names — never `--subnet` flags\n/// (M7 plan decision 3).\n#[derive(Subcommand)]\nenum SubnetCmd {\n    /// Show subnet membership: name, paired nodes, endpoints.\n    ///\n    /// Never prints seeds, epochs, or pairing codes. Bare `spt subnet` is\n…\n}","startLine":220,"lineNumbers":[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,null,974,null,976,null,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,null,1163]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"}}},"isError":false,"timestamp":1784196022391}}
{"type":"message","id":"cc518e88","parentId":"0944e8e2","timestamp":"2026-07-16T10:00:22.397Z","message":{"role":"toolResult","toolCallId":"call_3wkp6dmH02pgRy9xAYTYRoAf|fc_0698befac979932d016a58abb5778c819a8f5933ffd4327e63","toolName":"grep","content":[{"type":"text","text":"# crates/spt/src/\n## cli.rs\n 5184|        }\n*5185|        if let Err(e) = spt_daemon::stop_daemon() {\n 5186|            eprintln!(\n 5187|                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n 5188|            );\n...\n 5202|    }\n*5203|    match spt_daemon::start_daemon() {\n 5204|        Ok(_) => {\n 5205|            let (version, product_version, already) = match applied {\n 5206|                O::AppliedDaemonless {"}],"details":{"scopePath":"crates/spt/src/cli.rs, crates/spt-daemon/src/applyhost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":2,"fileCount":1,"files":["crates/spt/src/cli.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":2}],"truncated":false,"displayContent":"# crates/spt/src/\n## cli.rs\n 5184│        }\n*5185│        if let Err(e) = spt_daemon::stop_daemon() {\n 5186│            eprintln!(\n 5187│                \"DAEMON_STOP_FAIL: {e} — the new version is on disk; run `spt daemon start` to finish.\"\n 5188│            );\n     │...\n 5202│    }\n*5203│    match spt_daemon::start_daemon() {\n 5204│        Ok(_) => {\n 5205│            let (version, product_version, already) = match applied {\n 5206│                O::AppliedDaemonless {"},"isError":false,"timestamp":1784196022397}}
{"type":"message","id":"d3441964","parentId":"cc518e88","timestamp":"2026-07-16T10:00:22.405Z","message":{"role":"toolResult","toolCallId":"call_SBVR9lquCbKlgcFq5sEne120|fc_0698befac979932d016a58abb577ac819a9e2f3db5ccee1785","toolName":"read","content":[{"type":"text","text":"fn classify_routes_a_node_label_feed_to_registry() {\n…\n    let inst = RegistryFeedRecord::Instance(RegistryUpdate {\n…\n        instance: Instance {\n…\n            adapter: None,\n            recent_projects: Vec::new(),\n            controlled: false,\n        },\n    });\n    let iline: serde_json::Value =\n        serde_json::from_slice(&inst.encode_line()[..inst.encode_line().len() - 1]).unwrap();\n    assert_eq!(classify_first_line(&iline), StreamFamily::Registry);\n\n    let junk: serde_json::Value = serde_json::json!({\"hello\": \"world\"});\n    assert_eq!(classify_first_line(&junk), StreamFamily::Unknown);\n}\n\n// [unit->REQ-NET-1] an unclassifiable stream is dropped without wedging the\n// dispatcher: garbage first, then a valid notif feed still converges.\n#[test]\nfn dispatcher_survives_an_unknown_stream() {\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    let mut a = connect_retry(&a_name);\n    let mut b_probe = connect_retry(&b_name);\n    seed_gates(\"d91-unk\", &node_hex(&mut a), &[]);\n\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    // Garbage stream: not JSON at all.\n    let junk = a\n        .net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op())))\n        .expect(\"open junk\");\n    a.net_stream_send(junk.stream_id, b\"this is not a protocol\\n\", None, true)\n        .expect(\"junk\");\n\n    // A valid feed afterward still lands — the loop did not die on the junk.\n    let row = NotifRow {\n        notif_id: \"ccdd:7\".to_string(),\n        subnet: \"d91-unk\".to_string(),\n        kind: \"agent\".to_string(),\n        from_id: \"oak@ccdd\".to_string(),\n        body: \"after the junk\".to_string(),\n        created_ms: 1_000,\n        dismissed: false,\n        seen: Default::default(),\n        last_surfaced_ms: 0,\n    };\n    let line = spt_net::net::ndjson::encode_line(&NotifRecord::Row { row });\n    let opened = a\n        .net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op())))\n        .expect(\"open feed\");\n    a.net_stream_send(opened.stream_id, &line, None, true)\n        .expect(\"send feed\");\n\n    let store_b = NotifStore::open().expect(\"B notif store\");\n    let mut found = false;\n    for _ in 0..400 {\n        if store_b\n            .list(\"d91-unk\")\n            .expect(\"list\")\n            .iter()\n            .any(|r| r.notif_id == \"ccdd:7\")\n        {\n            found = true;\n            break;\n        }\n        thread::sleep(Duration::from_millis(25));\n    }\n    assert!(\n        found,\n        \"the dispatcher survived the unknown stream and served the next one\"\n    );\n    stop.store(true, Ordering::Relaxed);\n}\n"}],"details":{"displayContent":{"text":"fn classify_routes_a_node_label_feed_to_registry() {\n…\n    let inst = RegistryFeedRecord::Instance(RegistryUpdate {\n…\n        instance: Instance {\n…\n            adapter: None,\r\n            recent_projects: Vec::new(),\r\n            controlled: false,\r\n        },\r\n    });\r\n    let iline: serde_json::Value =\r\n        serde_json::from_slice(&inst.encode_line()[..inst.encode_line().len() - 1]).unwrap();\r\n    assert_eq!(classify_first_line(&iline), StreamFamily::Registry);\r\n\r\n    let junk: serde_json::Value = serde_json::json!({\"hello\": \"world\"});\r\n    assert_eq!(classify_first_line(&junk), StreamFamily::Unknown);\r\n}\r\n\r\n// [unit->REQ-NET-1] an unclassifiable stream is dropped without wedging the\r\n// dispatcher: garbage first, then a valid notif feed still converges.\r\n#[test]\r\nfn dispatcher_survives_an_unknown_stream() {\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    let mut a = connect_retry(&a_name);\r\n    let mut b_probe = connect_retry(&b_name);\r\n    seed_gates(\"d91-unk\", &node_hex(&mut a), &[]);\r\n\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    // Garbage stream: not JSON at all.\r\n    let junk = a\r\n        .net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op())))\r\n        .expect(\"open junk\");\r\n    a.net_stream_send(junk.stream_id, b\"this is not a protocol\\n\", None, true)\r\n        .expect(\"junk\");\r\n\r\n    // A valid feed afterward still lands — the loop did not die on the junk.\r\n    let row = NotifRow {\r\n        notif_id: \"ccdd:7\".to_string(),\r\n        subnet: \"d91-unk\".to_string(),\r\n        kind: \"agent\".to_string(),\r\n        from_id: \"oak@ccdd\".to_string(),\r\n        body: \"after the junk\".to_string(),\r\n        created_ms: 1_000,\r\n        dismissed: false,\r\n        seen: Default::default(),\r\n        last_surfaced_ms: 0,\r\n    };\r\n    let line = spt_net::net::ndjson::encode_line(&NotifRecord::Row { row });\r\n    let opened = a\r\n        .net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op())))\r\n        .expect(\"open feed\");\r\n    a.net_stream_send(opened.stream_id, &line, None, true)\r\n        .expect(\"send feed\");\r\n\r\n    let store_b = NotifStore::open().expect(\"B notif store\");\r\n    let mut found = false;\r\n    for _ in 0..400 {\r\n        if store_b\r\n            .list(\"d91-unk\")\r\n            .expect(\"list\")\r\n            .iter()\r\n            .any(|r| r.notif_id == \"ccdd:7\")\r\n        {\r\n            found = true;\r\n            break;\r\n        }\r\n        thread::sleep(Duration::from_millis(25));\r\n    }\r\n    assert!(\r\n        found,\r\n        \"the dispatcher survived the unknown stream and served the next one\"\r\n    );\r\n    stop.store(true, Ordering::Relaxed);\r\n}\r\n","startLine":1044,"lineNumbers":[1044,null,1063,null,1066,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\dispatch.rs"}}},"isError":false,"timestamp":1784196022405}}
{"type":"message","id":"0a8b6bc4","parentId":"d3441964","timestamp":"2026-07-16T10:00:22.529Z","message":{"role":"toolResult","toolCallId":"call_tMZe8IrmI2nsYOE4q1tzLmwL|fc_0698befac979932d016a58abb5777c819abdc78dcea96273c7","toolName":"read","content":[{"type":"text","text":"fn cmd_update_fetch(channel: Option<String>, tag: Option<String>, apply: bool) -> i32 {\n…\n    let meta = match spt_daemon::release::verify_update_set_metadata(&signed, &policy) {\n…\n        Err(reason) => {\n…\n            // (REQ-UPDATE-FETCH-APPLY-FLAG).\n            let class = classify_fetch_reject(&reason, cache.applied_version(), cache.staged_version());\n            return match fetch_reject_action(class, apply) {\n                FetchAction::Apply => cmd_update_apply(false),\n                FetchAction::DoneOk => {\n                    match class {\n                        FetchRejectClass::AlreadyStaged { candidate } => println!(\n                            \"Update (counter {candidate}) is already downloaded. \\\n                             Run `spt update apply` to install it.\"\n                        ),\n                        FetchRejectClass::AlreadyApplied { candidate } => {\n                            println!(\"Already up to date (counter {candidate}).\")\n                        }\n                        FetchRejectClass::GenuineError => {}\n                    }\n                    0\n                }\n                // A genuine reject prints the friendly Display (not Debug) and\n                // stays an error; --apply never forces past a failed verify.\n                FetchAction::Error => {\n                    eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\n                    1\n                }\n            };\n        }\n    };\n\n    // 3. Download EVERY platform artifact the set names, verifying each against\n    //    its signed digest before it lands in the cache (so this node can later\n    //    re-serve any platform to peers without serving poison).\n    let mut artifacts: std::collections::BTreeMap<String, Vec<u8>> =\n        std::collections::BTreeMap::new();\n    for (triple, art) in &meta.artifacts {\n        let Some(asset) = art.asset_name.as_deref() else {\n            eprintln!(\"UPDATE_FETCH_FAIL: set artifact for {triple} has no asset_name\");\n            return 1;\n        };\n        eprintln!(\"  fetching {asset} ({triple})…\");\n        // [impl->REQ-UPDATE-GH-TRANSPORT] asset bytes ride the gh carrier; the\n        // signed-digest verify below is carrier-independent (ADR-0036 §3).\n        let bytes = match fetch_release_asset_bytes(\n            &repo,\n            tag.as_deref(),\n            asset,\n            EffectiveTransport::Gh,\n            &scratch,\n        ) {\n            Ok(b) => b,\n            Err(e) => {\n                let _ = std::fs::remove_dir_all(&scratch);\n                eprintln!(\"UPDATE_FETCH_FAIL: {asset} from {repo}: {e}\");\n                return 1;\n            }\n        };\n        if let Err(reason) =\n            spt_daemon::release::verify_update_set_artifact(&meta, triple, &bytes)\n        {\n            let _ = std::fs::remove_dir_all(&scratch);\n            // Friendly Display, not the raw enum Debug (REQ-UPDATE-FETCH-CURRENT-UX);\n            // a genuine artifact failure stays an error.\n            eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\n            return 1;\n        }\n        artifacts.insert(triple.clone(), bytes);\n    }\n\n    // 3b. The docs bundle (ADR-0036 §4) — best-effort by BINDING: any docs\n    //     failure here prints the machine-greppable UPDATE_DOCS_SKIPPED and\n    //     the binary staging below proceeds UNCHANGED (retried next fetch).\n    // [impl->REQ-DOCS-RELEASE-ASSET]\n    match &meta.docs {\n        None => {}\n        Some(docs) => {\n            eprintln!(\"  fetching {} (docs bundle)…\", docs.asset_name);\n            match fetch_release_asset_bytes(\n                &repo,\n                tag.as_deref(),\n                &docs.asset_name,\n                EffectiveTransport::Gh,\n                &scratch,\n            ) {\n                Ok(bundle) => match spt_daemon::verify_update_set_docs(&meta, &bundle) {\n                    Ok(()) => {\n                        if let Err(e) = cache.stage_docs(&bundle) {\n                            eprintln!(\"UPDATE_DOCS_SKIPPED: stage: {e} — docs retry next fetch\");\n                        }\n                    }\n                    Err(reason) => eprintln!(\n                        \"UPDATE_DOCS_SKIPPED: {reason} — docs retry next fetch\"\n                    ),\n                },\n                Err(e) => eprintln!(\n                    \"UPDATE_DOCS_SKIPPED: {} from {repo}: {e} — docs retry next fetch\",\n                    docs.asset_name\n                ),\n            }\n        }\n    }\n    let _ = std::fs::remove_dir_all(&scratch);\n\n    // 4. The full per-node front door for THIS platform (selects + classifies)\n    //    — mirrors what apply will do, so a set with no artifact for this node\n    //    fails loud here, not silently at apply.\n    let local = spt_daemon::current_platform();\n    let Some(local_bytes) = artifacts.get(local) else {\n        eprintln!(\"UPDATE_FETCH_REJECTED:NoArtifactForPlatform({local:?})\");\n        return 1;\n    };\n    match spt_daemon::update::plan_verified_update_set(\n        &spt_daemon::update::BrokerAbi::current(),\n        &signed,\n        local,\n        local_bytes,\n        &policy,\n    ) {\n        Ok(_plan) => {\n            if let Err(e) = cache.stage_update_set(&signed, &artifacts) {\n                eprintln!(\"UPDATE_FETCH_FAIL: stage: {e}\");\n                return 1;\n            }\n            let v = cache\n                .staged_version()\n                .map(|v| v.to_string())\n                .unwrap_or_else(|| \"?\".to_string());\n            println!(\n                \"UPDATE_STAGED:{v} (set, {} platforms, from {repo}) — run `spt update apply` to apply\",\n                meta.artifacts.len()\n            );\n            // --apply: one-shot get-to-latest — install the freshly-staged set now\n            // (REQ-UPDATE-FETCH-APPLY-FLAG). apply owns its verify + two-phase +\n            // auto-rollback.\n            if apply {\n                return cmd_update_apply(false);\n            }\n            0\n        }\n        Err(reason) => {\n            // Friendly Display, not the raw enum Debug (REQ-UPDATE-FETCH-CURRENT-UX).\n            eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\n            1\n        }\n    }\n}\n\n/// Epoch milliseconds (the notif surfacing clock).\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/// Resolve the sender id: explicit `--from`, else session auto-detect, else `\"\"`.\n/// The from-less `\"\"` is the CLI-origin seam: `cmd_send` stamps `cli@<node-label>`\n/// on it (REQ-MSG-CLI-ORIGIN), while `cmd_ring` keeps refusing it (a ring needs a\n\n[Showing lines 6199-6353 of 18900. Use :6354 to continue]"}],"details":{"truncation":{"content":"            // (REQ-UPDATE-FETCH-APPLY-FLAG).\r\n            let class = classify_fetch_reject(&reason, cache.applied_version(), cache.staged_version());\r\n            return match fetch_reject_action(class, apply) {\r\n                FetchAction::Apply => cmd_update_apply(false),\r\n                FetchAction::DoneOk => {\r\n                    match class {\r\n                        FetchRejectClass::AlreadyStaged { candidate } => println!(\r\n                            \"Update (counter {candidate}) is already downloaded. \\\r\n                             Run `spt update apply` to install it.\"\r\n                        ),\r\n                        FetchRejectClass::AlreadyApplied { candidate } => {\r\n                            println!(\"Already up to date (counter {candidate}).\")\r\n                        }\r\n                        FetchRejectClass::GenuineError => {}\r\n                    }\r\n                    0\r\n                }\r\n                // A genuine reject prints the friendly Display (not Debug) and\r\n                // stays an error; --apply never forces past a failed verify.\r\n                FetchAction::Error => {\r\n                    eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\r\n                    1\r\n                }\r\n            };\r\n        }\r\n    };\r\n\r\n    // 3. Download EVERY platform artifact the set names, verifying each against\r\n    //    its signed digest before it lands in the cache (so this node can later\r\n    //    re-serve any platform to peers without serving poison).\r\n    let mut artifacts: std::collections::BTreeMap<String, Vec<u8>> =\r\n        std::collections::BTreeMap::new();\r\n    for (triple, art) in &meta.artifacts {\r\n        let Some(asset) = art.asset_name.as_deref() else {\r\n            eprintln!(\"UPDATE_FETCH_FAIL: set artifact for {triple} has no asset_name\");\r\n            return 1;\r\n        };\r\n        eprintln!(\"  fetching {asset} ({triple})…\");\r\n        // [impl->REQ-UPDATE-GH-TRANSPORT] asset bytes ride the gh carrier; the\r\n        // signed-digest verify below is carrier-independent (ADR-0036 §3).\r\n        let bytes = match fetch_release_asset_bytes(\r\n            &repo,\r\n            tag.as_deref(),\r\n            asset,\r\n            EffectiveTransport::Gh,\r\n            &scratch,\r\n        ) {\r\n            Ok(b) => b,\r\n            Err(e) => {\r\n                let _ = std::fs::remove_dir_all(&scratch);\r\n                eprintln!(\"UPDATE_FETCH_FAIL: {asset} from {repo}: {e}\");\r\n                return 1;\r\n            }\r\n        };\r\n        if let Err(reason) =\r\n            spt_daemon::release::verify_update_set_artifact(&meta, triple, &bytes)\r\n        {\r\n            let _ = std::fs::remove_dir_all(&scratch);\r\n            // Friendly Display, not the raw enum Debug (REQ-UPDATE-FETCH-CURRENT-UX);\r\n            // a genuine artifact failure stays an error.\r\n            eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\r\n            return 1;\r\n        }\r\n        artifacts.insert(triple.clone(), bytes);\r\n    }\r\n\r\n    // 3b. The docs bundle (ADR-0036 §4) — best-effort by BINDING: any docs\r\n    //     failure here prints the machine-greppable UPDATE_DOCS_SKIPPED and\r\n    //     the binary staging below proceeds UNCHANGED (retried next fetch).\r\n    // [impl->REQ-DOCS-RELEASE-ASSET]\r\n    match &meta.docs {\r\n        None => {}\r\n        Some(docs) => {\r\n            eprintln!(\"  fetching {} (docs bundle)…\", docs.asset_name);\r\n            match fetch_release_asset_bytes(\r\n                &repo,\r\n                tag.as_deref(),\r\n                &docs.asset_name,\r\n                EffectiveTransport::Gh,\r\n                &scratch,\r\n            ) {\r\n                Ok(bundle) => match spt_daemon::verify_update_set_docs(&meta, &bundle) {\r\n                    Ok(()) => {\r\n                        if let Err(e) = cache.stage_docs(&bundle) {\r\n                            eprintln!(\"UPDATE_DOCS_SKIPPED: stage: {e} — docs retry next fetch\");\r\n                        }\r\n                    }\r\n                    Err(reason) => eprintln!(\r\n                        \"UPDATE_DOCS_SKIPPED: {reason} — docs retry next fetch\"\r\n                    ),\r\n                },\r\n                Err(e) => eprintln!(\r\n                    \"UPDATE_DOCS_SKIPPED: {} from {repo}: {e} — docs retry next fetch\",\r\n                    docs.asset_name\r\n                ),\r\n            }\r\n        }\r\n    }\r\n    let _ = std::fs::remove_dir_all(&scratch);\r\n\r\n    // 4. The full per-node front door for THIS platform (selects + classifies)\r\n    //    — mirrors what apply will do, so a set with no artifact for this node\r\n    //    fails loud here, not silently at apply.\r\n    let local = spt_daemon::current_platform();\r\n    let Some(local_bytes) = artifacts.get(local) else {\r\n        eprintln!(\"UPDATE_FETCH_REJECTED:NoArtifactForPlatform({local:?})\");\r\n        return 1;\r\n    };\r\n    match spt_daemon::update::plan_verified_update_set(\r\n        &spt_daemon::update::BrokerAbi::current(),\r\n        &signed,\r\n        local,\r\n        local_bytes,\r\n        &policy,\r\n    ) {\r\n        Ok(_plan) => {\r\n            if let Err(e) = cache.stage_update_set(&signed, &artifacts) {\r\n                eprintln!(\"UPDATE_FETCH_FAIL: stage: {e}\");\r\n                return 1;\r\n            }\r\n            let v = cache\r\n                .staged_version()\r\n                .map(|v| v.to_string())\r\n                .unwrap_or_else(|| \"?\".to_string());\r\n            println!(\r\n                \"UPDATE_STAGED:{v} (set, {} platforms, from {repo}) — run `spt update apply` to apply\",\r\n                meta.artifacts.len()\r\n            );\r\n            // --apply: one-shot get-to-latest — install the freshly-staged set now\r\n            // (REQ-UPDATE-FETCH-APPLY-FLAG). apply owns its verify + two-phase +\r\n            // auto-rollback.\r\n            if apply {\r\n                return cmd_update_apply(false);\r\n            }\r\n            0\r\n        }\r\n        Err(reason) => {\r\n            // Friendly Display, not the raw enum Debug (REQ-UPDATE-FETCH-CURRENT-UX).\r\n            eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// Epoch milliseconds (the notif surfacing clock).\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/// Resolve the sender id: explicit `--from`, else session auto-detect, else `\"\"`.\r\n/// The from-less `\"\"` is the CLI-origin seam: `cmd_send` stamps `cli@<node-label>`\r\n/// on it (REQ-MSG-CLI-ORIGIN), while `cmd_ring` keeps refusing it (a ring needs a\r","truncated":true,"truncatedBy":"lines","totalLines":12702,"totalBytes":6578,"outputLines":155,"outputBytes":6578,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn cmd_update_fetch(channel: Option<String>, tag: Option<String>, apply: bool) -> i32 {\n…\n    let meta = match spt_daemon::release::verify_update_set_metadata(&signed, &policy) {\n…\n        Err(reason) => {\n…\n            // (REQ-UPDATE-FETCH-APPLY-FLAG).\r\n            let class = classify_fetch_reject(&reason, cache.applied_version(), cache.staged_version());\r\n            return match fetch_reject_action(class, apply) {\r\n                FetchAction::Apply => cmd_update_apply(false),\r\n                FetchAction::DoneOk => {\r\n                    match class {\r\n                        FetchRejectClass::AlreadyStaged { candidate } => println!(\r\n                            \"Update (counter {candidate}) is already downloaded. \\\r\n                             Run `spt update apply` to install it.\"\r\n                        ),\r\n                        FetchRejectClass::AlreadyApplied { candidate } => {\r\n                            println!(\"Already up to date (counter {candidate}).\")\r\n                        }\r\n                        FetchRejectClass::GenuineError => {}\r\n                    }\r\n                    0\r\n                }\r\n                // A genuine reject prints the friendly Display (not Debug) and\r\n                // stays an error; --apply never forces past a failed verify.\r\n                FetchAction::Error => {\r\n                    eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\r\n                    1\r\n                }\r\n            };\r\n        }\r\n    };\r\n\r\n    // 3. Download EVERY platform artifact the set names, verifying each against\r\n    //    its signed digest before it lands in the cache (so this node can later\r\n    //    re-serve any platform to peers without serving poison).\r\n    let mut artifacts: std::collections::BTreeMap<String, Vec<u8>> =\r\n        std::collections::BTreeMap::new();\r\n    for (triple, art) in &meta.artifacts {\r\n        let Some(asset) = art.asset_name.as_deref() else {\r\n            eprintln!(\"UPDATE_FETCH_FAIL: set artifact for {triple} has no asset_name\");\r\n            return 1;\r\n        };\r\n        eprintln!(\"  fetching {asset} ({triple})…\");\r\n        // [impl->REQ-UPDATE-GH-TRANSPORT] asset bytes ride the gh carrier; the\r\n        // signed-digest verify below is carrier-independent (ADR-0036 §3).\r\n        let bytes = match fetch_release_asset_bytes(\r\n            &repo,\r\n            tag.as_deref(),\r\n            asset,\r\n            EffectiveTransport::Gh,\r\n            &scratch,\r\n        ) {\r\n            Ok(b) => b,\r\n            Err(e) => {\r\n                let _ = std::fs::remove_dir_all(&scratch);\r\n                eprintln!(\"UPDATE_FETCH_FAIL: {asset} from {repo}: {e}\");\r\n                return 1;\r\n            }\r\n        };\r\n        if let Err(reason) =\r\n            spt_daemon::release::verify_update_set_artifact(&meta, triple, &bytes)\r\n        {\r\n            let _ = std::fs::remove_dir_all(&scratch);\r\n            // Friendly Display, not the raw enum Debug (REQ-UPDATE-FETCH-CURRENT-UX);\r\n            // a genuine artifact failure stays an error.\r\n            eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\r\n            return 1;\r\n        }\r\n        artifacts.insert(triple.clone(), bytes);\r\n    }\r\n\r\n    // 3b. The docs bundle (ADR-0036 §4) — best-effort by BINDING: any docs\r\n    //     failure here prints the machine-greppable UPDATE_DOCS_SKIPPED and\r\n    //     the binary staging below proceeds UNCHANGED (retried next fetch).\r\n    // [impl->REQ-DOCS-RELEASE-ASSET]\r\n    match &meta.docs {\r\n        None => {}\r\n        Some(docs) => {\r\n            eprintln!(\"  fetching {} (docs bundle)…\", docs.asset_name);\r\n            match fetch_release_asset_bytes(\r\n                &repo,\r\n                tag.as_deref(),\r\n                &docs.asset_name,\r\n                EffectiveTransport::Gh,\r\n                &scratch,\r\n            ) {\r\n                Ok(bundle) => match spt_daemon::verify_update_set_docs(&meta, &bundle) {\r\n                    Ok(()) => {\r\n                        if let Err(e) = cache.stage_docs(&bundle) {\r\n                            eprintln!(\"UPDATE_DOCS_SKIPPED: stage: {e} — docs retry next fetch\");\r\n                        }\r\n                    }\r\n                    Err(reason) => eprintln!(\r\n                        \"UPDATE_DOCS_SKIPPED: {reason} — docs retry next fetch\"\r\n                    ),\r\n                },\r\n                Err(e) => eprintln!(\r\n                    \"UPDATE_DOCS_SKIPPED: {} from {repo}: {e} — docs retry next fetch\",\r\n                    docs.asset_name\r\n                ),\r\n            }\r\n        }\r\n    }\r\n    let _ = std::fs::remove_dir_all(&scratch);\r\n\r\n    // 4. The full per-node front door for THIS platform (selects + classifies)\r\n    //    — mirrors what apply will do, so a set with no artifact for this node\r\n    //    fails loud here, not silently at apply.\r\n    let local = spt_daemon::current_platform();\r\n    let Some(local_bytes) = artifacts.get(local) else {\r\n        eprintln!(\"UPDATE_FETCH_REJECTED:NoArtifactForPlatform({local:?})\");\r\n        return 1;\r\n    };\r\n    match spt_daemon::update::plan_verified_update_set(\r\n        &spt_daemon::update::BrokerAbi::current(),\r\n        &signed,\r\n        local,\r\n        local_bytes,\r\n        &policy,\r\n    ) {\r\n        Ok(_plan) => {\r\n            if let Err(e) = cache.stage_update_set(&signed, &artifacts) {\r\n                eprintln!(\"UPDATE_FETCH_FAIL: stage: {e}\");\r\n                return 1;\r\n            }\r\n            let v = cache\r\n                .staged_version()\r\n                .map(|v| v.to_string())\r\n                .unwrap_or_else(|| \"?\".to_string());\r\n            println!(\r\n                \"UPDATE_STAGED:{v} (set, {} platforms, from {repo}) — run `spt update apply` to apply\",\r\n                meta.artifacts.len()\r\n            );\r\n            // --apply: one-shot get-to-latest — install the freshly-staged set now\r\n            // (REQ-UPDATE-FETCH-APPLY-FLAG). apply owns its verify + two-phase +\r\n            // auto-rollback.\r\n            if apply {\r\n                return cmd_update_apply(false);\r\n            }\r\n            0\r\n        }\r\n        Err(reason) => {\r\n            // Friendly Display, not the raw enum Debug (REQ-UPDATE-FETCH-CURRENT-UX).\r\n            eprintln!(\"UPDATE_FETCH_REJECTED: {reason}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// Epoch milliseconds (the notif surfacing clock).\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/// Resolve the sender id: explicit `--from`, else session auto-detect, else `\"\"`.\r\n/// The from-less `\"\"` is the CLI-origin seam: `cmd_send` stamps `cli@<node-label>`\r\n/// on it (REQ-MSG-CLI-ORIGIN), while `cmd_ring` keeps refusing it (a ring needs a\r","startLine":6110,"lineNumbers":[6110,null,6191,null,6193,null,6199,6200,6201,6202,6203,6204,6205,6206,6207,6208,6209,6210,6211,6212,6213,6214,6215,6216,6217,6218,6219,6220,6221,6222,6223,6224,6225,6226,6227,6228,6229,6230,6231,6232,6233,6234,6235,6236,6237,6238,6239,6240,6241,6242,6243,6244,6245,6246,6247,6248,6249,6250,6251,6252,6253,6254,6255,6256,6257,6258,6259,6260,6261,6262,6263,6264,6265,6266,6267,6268,6269,6270,6271,6272,6273,6274,6275,6276,6277,6278,6279,6280,6281,6282,6283,6284,6285,6286,6287,6288,6289,6290,6291,6292,6293,6294,6295,6296,6297,6298,6299,6300,6301,6302,6303,6304,6305,6306,6307,6308,6309,6310,6311,6312,6313,6314,6315,6316,6317,6318,6319,6320,6321,6322,6323,6324,6325,6326,6327,6328,6329,6330,6331,6332,6333,6334,6335,6336,6337,6338,6339,6340,6341,6342,6343,6344,6345,6346,6347,6348,6349,6350,6351,6352,6353]},"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":6578,"outputLines":155,"outputBytes":6578,"shownRange":{"start":6199,"end":6353},"nextOffset":6354}}},"isError":false,"timestamp":1784196022529}}
{"type":"message","id":"735857a2","parentId":"0a8b6bc4","timestamp":"2026-07-16T10:00:34.281Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning deterministic feedback loop**"},{"type":"toolCall","id":"call_MwfqBd0c2MuDK4mQ1s3k9dxm|fc_0698befac979932d016a58abc1b2cc819a807a157e49dd2513","name":"grep","arguments":{"i":"Finding stream ring test seam","pattern":"ring_cap|set_.*ring|DEFAULT_STREAM_RING_CHUNKS|with_.*ring","path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_bPpNWQjbtvqfdiMaS4hAOdTn|fc_0698befac979932d016a58abc1b2e0819aa063659ae531382c","name":"grep","arguments":{"i":"Finding dispatcher injectable worker seams","pattern":"run_dispatch_loop|spawn_dispatcher_for|DEFAULT_DISPATCH_POLL|worker\\(","path":"crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/src/dispatch.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_WudS0ympxzFJEIa4jL6mR8xl|fc_0698befac979932d016a58abc1b2ec819aae45b347e2fb16e4","name":"grep","arguments":{"i":"Finding daemon refresh log instrumentation","pattern":"BRAIN_UP|BRAIN_UPDATE_RESTART|BRAIN_RESTART|DISPATCH:|stream-sub-attach|CONN_|BRAIN_RESUMED","path":"crates/spt-daemon/src;crates/spt/src","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":9352,"output":392,"cacheRead":185856,"cacheWrite":0,"totalTokens":195600,"reasoningTokens":126,"cost":{"input":0.04676,"output":0.01176,"cacheRead":0.092928,"cacheWrite":0,"total":0.151448}},"stopReason":"toolUse","timestamp":1784196022573,"responseId":"resp_0698befac979932d016a58abb70640819aba4800e2f893b4fc","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58abbad644819abd6a15a936c01daa","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvBYm5yvDAs9q21hRs8SVluWhLxATnjZ5s7gcnTfSWu6m7MA53zoy0fYmPglp50-8AfaamcjsNIkKm_WctPwtX_brF5QjGpLGeSh4WyQdxL3msPFVSfXs6asXhOwL_uHfqQ0oM_rgzGQJsxlcnNFNjly-Sv3TRVAhnL8vXe4BCxfi1X8arGDvrR5LH4yPGnOEMmvEDQ-kIoooFwvJ49B-ADcUH06HqzanFiGAiqHLBWjKvVjRRHlQZnQvzdgKIvl5eCoC4pS81_bXODYWztuBcUWVLjIimQpZ794-KPGW42DPx88F6_an_mk5wrLbQ00OtAA66uoJZ2HPaD_UE5lmggL_StmRwOMR72PUfxLNv_ihx1Eaz0VNvxFBuuoXQ4rcBBUkWA5GWUsY4fDX3x5JiVlpJI02Jf6xukCgBXrLyCsK2F6Xc4OtpR2KjmrhH5iQeOASuRWfvxX6Clm1o5Wej-8TBXB2YewldqEJEsQgxpmtGuf8wn4XDkdgXfSGSTn6ruhlCBwVlDDd1JjgcMebCtQLsvCbEhs8umoge3Iw5aLeyeqx0eXylJOWTKxpnL2wUh8xlG3qd_IgKtrqk3CTfDf1M5Uut4-NkJEOxKWpODNgFe78oVGw4RVNUIVfvLzZfPRkyilP7qd4ICZgcqhEN6NTGwWBBJenbApkVOK-eAkemzVdfLhJPV2NGk4M4XUqEM08rxPf0xyGspnUpkOYDQKMvi4IAVKW4nF0tGTW9pjbz0sHwEfkiy3iNLYN41DOK9LxF8XIqenTTm9llFTG9ZfigMfkhGRfWoloqpCXu8mhUh15Dk1FJqpjCzbYcuQj6wf7gEw6pJKtv_88c0EdA53vHJLYaLEzsDGpsor92hVYkkviHVmfaTMyWNaXeRQiZ1M10o0kfH07Mg7khesRzYT_ZGg1mOvMru0E_hS4IRTR77_mYnOtWokVay2DdcF43vA6_Cors4nkVYJ95JYsqL9vi4R19nseApJdeN7ZzUplMekqY3pnepDIt-iSmLIKrTsE4f_GvRBGlUEWttLUjGSue6H5OzlaOkpbtMiPZQqwDe2d2XB0p8vEcRRMnng31xwvZOpPrDIS1JegpOazijBCqK6RBEnoVIhYXfiZYaRWyl0H5Utectdc5w0al8y2RUG7uRcL8PFePwpVy3WzIAV6vmNBHvaTjXBKrDrIa4FDaI4bq1h81HRtRi0B4haPnzHTTzE7lOLHTUwdA1ZVhLAvWhGa7vXMoqjKW3J-Wb1XJWgUMlZmhL8ytrA9axOhs6PEQCT6EbVvyHAsP_hkobtUNf7O3K9FbsnUOVbG0tnrkB2b9XBmag5v7WMs4WPxDt7NcPUg9WTGw2U8ZQEo_iBdIkeVx4TqMePWzHa0ChQUt-2RWZiOAxyxjXT0nLTQxtlbop_gwLB954v22levSC4i9naHo07tGeRDwyKnPvOnx97sU0Wqe6GcnmrhT2qGnX9RnFPTHcOdnEJ3nK8OTn0LLJD4Ewu4L0ieo4OTSIlPZmxUHV66KrnhyLyLLvts-QgUbbr-ZHLoXQDw83vqzeIMYat_EPGSHI3R_xq5jnSbF2DFkGPbvCIeHNllJeiTagZKIb_qbx5aBF4wFc8J2tFX2H813ubuvjkUgZOiDN_gKuSIj4CgykeKPmJxzH7o1fcc9zvfWKP3jwFA79vvgAwsC2n7Rz5X3SlP3WIPPqdyqNphKbkhgBYLFZONMqafn9","summary":[{"type":"summary_text","text":"**Planning deterministic feedback loop**"}]},{"id":"fc_0698befac979932d016a58abc1b2cc819a807a157e49dd2513","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding stream ring test seam\",\"pattern\":\"ring_cap|set_.*ring|DEFAULT_STREAM_RING_CHUNKS|with_.*ring\",\"path\":\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_MwfqBd0c2MuDK4mQ1s3k9dxm","name":"grep"},{"id":"fc_0698befac979932d016a58abc1b2e0819aa063659ae531382c","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatcher injectable worker seams\",\"pattern\":\"run_dispatch_loop|spawn_dispatcher_for|DEFAULT_DISPATCH_POLL|worker\\\\(\",\"path\":\"crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/src/dispatch.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_bPpNWQjbtvqfdiMaS4hAOdTn","name":"grep"},{"id":"fc_0698befac979932d016a58abc1b2ec819aae45b347e2fb16e4","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding daemon refresh log instrumentation\",\"pattern\":\"BRAIN_UP|BRAIN_UPDATE_RESTART|BRAIN_RESTART|DISPATCH:|stream-sub-attach|CONN_|BRAIN_RESUMED\",\"path\":\"crates/spt-daemon/src;crates/spt/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_WudS0ympxzFJEIa4jL6mR8xl","name":"grep"}]},"duration":11680.437800000422,"ttft":4247.714399999939,"contextSnapshot":{"promptTokens":195208,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MwfqBd0c2MuDK4mQ1s3k9dxm|fc_0698befac979932d016a58abc1b2cc819a807a157e49dd2513","toolName":"grep","startedAt":"2026-07-16T10:00:34.281Z","args":{"path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests"},"intent":"Finding stream ring test seam"},"id":"7832f6a9","parentId":"735857a2","timestamp":"2026-07-16T10:00:34.281Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bPpNWQjbtvqfdiMaS4hAOdTn|fc_0698befac979932d016a58abc1b2e0819aa063659ae531382c","toolName":"grep","startedAt":"2026-07-16T10:00:34.281Z","args":{"path":"crates/spt-daemon/tests/dispatch.rs;crates/spt-daemon/src/dispatch.rs"},"intent":"Finding dispatcher injectable worker seams"},"id":"af980572","parentId":"7832f6a9","timestamp":"2026-07-16T10:00:34.281Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WudS0ympxzFJEIa4jL6mR8xl|fc_0698befac979932d016a58abc1b2ec819aae45b347e2fb16e4","toolName":"grep","startedAt":"2026-07-16T10:00:34.281Z","args":{"path":"crates/spt-daemon/src;crates/spt/src"},"intent":"Finding daemon refresh log instrumentation"},"id":"ed5f78af","parentId":"af980572","timestamp":"2026-07-16T10:00:34.281Z"}
{"type":"message","id":"8fc877eb","parentId":"ed5f78af","timestamp":"2026-07-16T10:00:34.289Z","message":{"role":"toolResult","toolCallId":"call_bPpNWQjbtvqfdiMaS4hAOdTn|fc_0698befac979932d016a58abc1b2e0819aa063659ae531382c","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## 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 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\n## src/\n### dispatch.rs\n 86|/// workers block on their stream's event feed).\n*87|pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n 88|\n 89|/// What protocol a peer-initiated stream speaks, by its first record's shape.\n 90|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n...\n 197|/// stream).\n*198|pub fn run_dispatch_loop(\n 199|    broker_name: &str,\n 200|    paths: &DispatchPaths,\n 201|    registry: Arc<RegistryHost>,\n...\n 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...\n 284|/// loop on a fresh brain connection.\n*285|fn worker(\n 286|    broker_name: &str,\n 287|    stream_id: u64,\n 288|    origin: &str,\n...\n 693|        if let Err(e) =\n*694|            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 695|        {\n 696|            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 697|        }"}],"details":{"scopePath":"crates/spt-daemon/tests/dispatch.rs, crates/spt-daemon/src/dispatch.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":20,"fileCount":2,"files":["crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/src/dispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/dispatch.rs","count":15},{"path":"crates/spt-daemon/src/dispatch.rs","count":5}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## 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 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\n## src/\n### dispatch.rs\n  86│/// workers block on their stream's event feed).\n *87│pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n  88│\n  89│/// What protocol a peer-initiated stream speaks, by its first record's shape.\n  90│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n    │...\n 197│/// stream).\n*198│pub fn run_dispatch_loop(\n 199│    broker_name: &str,\n 200│    paths: &DispatchPaths,\n 201│    registry: Arc<RegistryHost>,\n    │...\n 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    │...\n 284│/// loop on a fresh brain connection.\n*285│fn worker(\n 286│    broker_name: &str,\n 287│    stream_id: u64,\n 288│    origin: &str,\n    │...\n 693│        if let Err(e) =\n*694│            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 695│        {\n 696│            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 697│        }"},"isError":false,"timestamp":1784196034288}}
{"type":"message","id":"8ca189ca","parentId":"8fc877eb","timestamp":"2026-07-16T10:00:34.297Z","message":{"role":"toolResult","toolCallId":"call_MwfqBd0c2MuDK4mQ1s3k9dxm|fc_0698befac979932d016a58abc1b2cc819a807a157e49dd2513","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\n 110|/// buffer without limit.\n*111|const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n 112|\n 113|/// Max bytes pulled per stream read (one ring chunk).\n 114|const STREAM_READ_CHUNK: usize = 64 * 1024;\n...\n 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## tests/\n### dispatch.rs\n 137|    for id in synced_ids {\n*138|        vis.set_sync_subnets(id, vec![subnet.to_string()]);\n 139|    }\n 140|    vis.save().expect(\"save visibility\");\n 141|}\n### propagate.rs\n 145|                    broker_resource_abi: BROKER_RESOURCE_ABI,\n*146|                    asset_name: Some(\"spt-current\".to_string()),\n 147|                },\n 148|            ),\n 149|            (\n...\n 154|                    broker_resource_abi: BROKER_RESOURCE_ABI,\n*155|                    asset_name: Some(\"spt-other\".to_string()),\n 156|                },\n 157|            ),\n 158|        ]),\n...\n 188|        broker_resource_abi: BROKER_RESOURCE_ABI,\n*189|        asset_name: Some(asset.to_string()),\n 190|    };\n 191|    let meta = UpdateSetMetadata {\n 192|        version: 6,\n### pump.rs\n 190|    let mut vis = VisibilityStore::load();\n*191|    vis.set_sync_subnets(\"ling-pumped\", vec![subnet.to_string()]);\n 192|    vis.save().expect(\"save visibility\");\n 193|\n 194|    let cs_b = ContextStore::open_or_init().expect(\"B context store\");\n...\n 299|    let mut a_vis = VisibilityStore::default();\n*300|    a_vis.set_sync_subnets(\"ling-pumped\", vec![subnet.to_string()]);\n 301|    a_vis.save_to(&a_paths.vis).expect(\"save A visibility\");\n 302|\n 303|    let a_store = NotifStore::open_at(&a_paths.notif_db).expect(\"A notif store\");\n...\n 546|    let mut a_vis = VisibilityStore::default();\n*547|    a_vis.set_sync_subnets(\"blackhole-ep\", vec![subnet.to_string()]);\n 548|    a_vis\n 549|        .save_to(&a_root.join(\"visibility.json\"))\n 550|        .expect(\"save vis\");\n...\n 732|    let mut a_vis = VisibilityStore::default();\n*733|    a_vis.set_sync_subnets(\"w2-ep\", vec![subnet.to_string()]);\n 734|    a_vis.save_to(&a_root.join(\"visibility.json\")).expect(\"save vis\");\n 735|\n 736|    let a_registry = Arc::new(RegistryHost::new_at(\n### sync.rs\n 143|    for id in synced_ids {\n*144|        vis.set_sync_subnets(id, vec![\"home\".to_string()]);\n 145|    }\n 146|    SyncPolicy {\n 147|        subnets: vec![(\"home\".to_string(), false)],"}],"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":14,"fileCount":5,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/propagate.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/sync.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":5},{"path":"crates/spt-daemon/tests/dispatch.rs","count":1},{"path":"crates/spt-daemon/tests/propagate.rs","count":3},{"path":"crates/spt-daemon/tests/pump.rs","count":4},{"path":"crates/spt-daemon/tests/sync.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\n 110│/// buffer without limit.\n*111│const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n 112│\n 113│/// Max bytes pulled per stream read (one ring chunk).\n 114│const STREAM_READ_CHUNK: usize = 64 * 1024;\n    │...\n 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## tests/\n### dispatch.rs\n 137│    for id in synced_ids {\n*138│        vis.set_sync_subnets(id, vec![subnet.to_string()]);\n 139│    }\n 140│    vis.save().expect(\"save visibility\");\n 141│}\n### propagate.rs\n 145│                    broker_resource_abi: BROKER_RESOURCE_ABI,\n*146│                    asset_name: Some(\"spt-current\".to_string()),\n 147│                },\n 148│            ),\n 149│            (\n    │...\n 154│                    broker_resource_abi: BROKER_RESOURCE_ABI,\n*155│                    asset_name: Some(\"spt-other\".to_string()),\n 156│                },\n 157│            ),\n 158│        ]),\n    │...\n 188│        broker_resource_abi: BROKER_RESOURCE_ABI,\n*189│        asset_name: Some(asset.to_string()),\n 190│    };\n 191│    let meta = UpdateSetMetadata {\n 192│        version: 6,\n### pump.rs\n 190│    let mut vis = VisibilityStore::load();\n*191│    vis.set_sync_subnets(\"ling-pumped\", vec![subnet.to_string()]);\n 192│    vis.save().expect(\"save visibility\");\n 193│\n 194│    let cs_b = ContextStore::open_or_init().expect(\"B context store\");\n    │...\n 299│    let mut a_vis = VisibilityStore::default();\n*300│    a_vis.set_sync_subnets(\"ling-pumped\", vec![subnet.to_string()]);\n 301│    a_vis.save_to(&a_paths.vis).expect(\"save A visibility\");\n 302│\n 303│    let a_store = NotifStore::open_at(&a_paths.notif_db).expect(\"A notif store\");\n    │...\n 546│    let mut a_vis = VisibilityStore::default();\n*547│    a_vis.set_sync_subnets(\"blackhole-ep\", vec![subnet.to_string()]);\n 548│    a_vis\n 549│        .save_to(&a_root.join(\"visibility.json\"))\n 550│        .expect(\"save vis\");\n    │...\n 732│    let mut a_vis = VisibilityStore::default();\n*733│    a_vis.set_sync_subnets(\"w2-ep\", vec![subnet.to_string()]);\n 734│    a_vis.save_to(&a_root.join(\"visibility.json\")).expect(\"save vis\");\n 735│\n 736│    let a_registry = Arc::new(RegistryHost::new_at(\n### sync.rs\n 143│    for id in synced_ids {\n*144│        vis.set_sync_subnets(id, vec![\"home\".to_string()]);\n 145│    }\n 146│    SyncPolicy {\n 147│        subnets: vec![(\"home\".to_string(), false)],"},"isError":false,"timestamp":1784196034297}}
{"type":"message","id":"06db4aba","parentId":"8ca189ca","timestamp":"2026-07-16T10:00:34.303Z","message":{"role":"toolResult","toolCallId":"call_WudS0ympxzFJEIa4jL6mR8xl|fc_0698befac979932d016a58abc1b2ec819aae45b347e2fb16e4","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## applyhost.rs\n 792|    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n*793|    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n 794|    // supervisor — the test/no-daemon shape) accepts the verb but acks\n 795|    // `honored: false`. This is the broker half of the apply trigger that\n 796|    // replaces the in-process handoff.\n## brain.rs\n 45|    ViewerEvictedEvent,\n*46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n*47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n 48|    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n 49|    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n 50|    KIND_NET_SENT,\n...\n 1459|    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n*1460|        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;\n 1461|        loop {\n 1462|            match self.read_event()? {\n*1463|                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n 1464|                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n 1465|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1466|                    return Ok(reply.honored);\n## brainproc.rs\n 106|/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n*107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n 108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n 110|/// between the broker serve path and the supervisor — both run in the broker\n...\n 162|    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n*163|    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n 164|    // [impl->REQ-DAEMON-STDERR-PERSIST]\n 165|    let _ = crate::stderrlog::install(\"brain\", generation);\n 166|    let name = broker_socket_name();\n...\n 175|    eprintln!(\n*176|        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n 177|        std::process::id(),\n 178|        reason.as_arg(),\n 179|    );\n...\n 204|            eprintln!(\n*205|                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n 206|                ids.len()\n 207|            )\n 208|        }\n...\n 887|                        if restart.take() {\n*888|                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n 889|                            let _ = child.kill();\n 890|                            let _ = child.wait();\n 891|                            planned = true;\n...\n 920|            backoff = base;\n*921|            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n 922|            continue;\n 923|        }\n 924|        reason = StartReason::Crash;\n 925|        backoff = next_backoff(backoff, started.elapsed(), base);\n*926|        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n 927|        sleep_backoff(backoff, stop);\n 928|    }\n 929|}\n...\n 1092|/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n*1093|/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n 1094|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1095|pub fn spawn_brain_supervisor(\n 1096|    stop: Arc<AtomicBool>,\n...\n 1205|    /// The planned-restart path (D3-3, Q3): a `BrainRestart::request` (the\n*1206|    /// broker's `KIND_BRAIN_RESTART` handler) makes the supervisor cycle the\n 1207|    /// brain and stamp the next spawn `Update` — distinguishing an intentional\n 1208|    /// update cycle from a crash (V2, broker-authoritative). A long-lived child\n 1209|    /// is used so the restart, not an exit, is what ends the cycle.\n## broker.rs\n 59|    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n*60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62|    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63|    KIND_NET_SENT,\n...\n 2541|    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542|    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543|    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n 2544|    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n 2545|    /// `Daemon::run` after it spawns the supervisor.\n...\n 2704|    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n*2705|    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n 2706|    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707|    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n 2708|    /// reports `honored: false` (no supervisor to respawn the child).\n...\n 3190|                }\n*3191|                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192|                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193|                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194|            }\n...\n 3893|        let frame = Envelope::new(\n*3894|            KIND_BRAIN_RESTARTED,\n 3895|            serde_json::to_value(BrainRestarted { honored })\n 3896|                .expect(\"BrainRestarted serializes\"),\n 3897|        );\n## conn.rs\n 74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n*75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n 78|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*79|static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n 80|\n 81|/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n 82|/// startup ([`init_log_anchor`], both broker and brain entries) so every\n...\n 141|struct Inner {\n*142|    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143|    id: u64,\n 144|    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145|    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n...\n 229|    /// Render the once-per-conn retirement record — the F-039 token split\n*230|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233|    /// already-dead counterpart, the offline-peer dial churn) emits the\n*234|    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n 235|    /// SAME attribution fields — quieter class, never thinner (the retired\n 236|    /// class is what discriminates fresh-carrier churn from interleave\n 237|    /// artifact in the field). Behavior is identical either way: poison,\n...\n 242|            format!(\n*243|                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 244|                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 245|                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 246|                self.attribution(),\n...\n 254|            format!(\n*255|                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n 256|                 broker conn poisoned + retired, never reused (same retire behavior \\\n 257|                 as POISONED — only the failure class differs) \\\n 258|                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n...\n 272|        eprintln!(\n*273|            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n 274|            self.attribution()\n 275|        );\n 276|    }\n...\n 287|            // First poison of this conn: loud, once. The token is class-split\n*288|            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n*289|            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 290|            eprintln!(\n 291|                \"{}\",\n 292|                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n...\n 428|        let inner = Arc::new(Inner {\n*429|            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n 430|            label: Mutex::new(String::new()),\n 431|            first_write_logged: AtomicBool::new(false),\n 432|            gate: Mutex::new(Gate { half: Some(half) }),\n...\n 707|    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*708|    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n*709|    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 710|    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 711|    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n 712|    // monotonic stamps. Quieter class, never thinner.\n...\n 721|        assert!(\n*722|            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n 723|            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 724|        );\n 725|        let retired = conn\n...\n 728|        assert!(\n*729|            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n 730|            \"organic class emits the DISTINCT retired token: {retired}\"\n 731|        );\n 732|        assert!(\n## daemon.rs\n 187|        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188|        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189|        let broker_handle: Option<Arc<Broker>> =\n 190|            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191|                Ok(broker) => {\n...\n 315|        // makes an update a *planned* restart on this same path: the supervisor's\n*316|        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n 317|        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n 318|        // onto the new binary with `start-reason=update`. The stop flag is held\n 319|        // for symmetry, never raised (the daemon runs until killed). The brain\n## dispatch.rs\n 225|                    DispatchOutcome::Served(_) => {}\n*226|                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\n 227|                }\n 228|            });\n 229|        }\n## msg.rs\n 231|/// which the caller surfaces — it never silently no-ops a swapped binary.\n*232|pub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n*233|/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n 234|/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235|/// acceptance, not completion. `honored` is false when the broker has no\n 236|/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237|/// reached a broker that cannot itself respawn.\n*238|pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239|\n 240|/// `spawn` payload — the program + args + initial geometry. The broker spawns\n 241|/// it under a PTY (no shell interpretation; argv is passed through).\n...\n 871|\n*872|/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n 873|/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874|/// cycle the child; false when the broker has none wired (a bare test broker),\n 875|/// so the caller can tell a real planned restart from a mere accept.\n## nethost.rs\n 263|        sub.lifecycle_event(\n*264|            \"stream-sub-attach\",\n 265|            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n 266|        );\n 267|        self.subscriber = Some(Arc::clone(&sub));"}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":42,"fileCount":9,"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/conn.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":1},{"path":"crates/spt-daemon/src/brain.rs","count":4},{"path":"crates/spt-daemon/src/brainproc.rs","count":9},{"path":"crates/spt-daemon/src/broker.rs","count":5},{"path":"crates/spt-daemon/src/conn.rs","count":15},{"path":"crates/spt-daemon/src/daemon.rs","count":2},{"path":"crates/spt-daemon/src/dispatch.rs","count":1},{"path":"crates/spt-daemon/src/msg.rs","count":4},{"path":"crates/spt-daemon/src/nethost.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## applyhost.rs\n 792│    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n*793│    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n 794│    // supervisor — the test/no-daemon shape) accepts the verb but acks\n 795│    // `honored: false`. This is the broker half of the apply trigger that\n 796│    // replaces the in-process handoff.\n## brain.rs\n   45│    ViewerEvictedEvent,\n  *46│    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n  *47│    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n   48│    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n   49│    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n   50│    KIND_NET_SENT,\n     │...\n 1459│    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n*1460│        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;\n 1461│        loop {\n 1462│            match self.read_event()? {\n*1463│                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n 1464│                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n 1465│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1466│                    return Ok(reply.honored);\n## brainproc.rs\n  106│/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n *107│/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n  108│/// its next wait tick, kills the current brain child, and respawns it with\n  109│/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n  110│/// between the broker serve path and the supervisor — both run in the broker\n     │...\n  162│    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n *163│    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n  164│    // [impl->REQ-DAEMON-STDERR-PERSIST]\n  165│    let _ = crate::stderrlog::install(\"brain\", generation);\n  166│    let name = broker_socket_name();\n     │...\n  175│    eprintln!(\n *176│        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n  177│        std::process::id(),\n  178│        reason.as_arg(),\n  179│    );\n     │...\n  204│            eprintln!(\n *205│                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n  206│                ids.len()\n  207│            )\n  208│        }\n     │...\n  887│                        if restart.take() {\n *888│                            eprintln!(\"BRAIN_UPDATE_RESTART: planned restart — cycling brain onto the swapped binary\");\n  889│                            let _ = child.kill();\n  890│                            let _ = child.wait();\n  891│                            planned = true;\n     │...\n  920│            backoff = base;\n *921│            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n  922│            continue;\n  923│        }\n  924│        reason = StartReason::Crash;\n  925│        backoff = next_backoff(backoff, started.elapsed(), base);\n *926│        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n  927│        sleep_backoff(backoff, stop);\n  928│    }\n  929│}\n     │...\n 1092│/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n*1093│/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n 1094│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1095│pub fn spawn_brain_supervisor(\n 1096│    stop: Arc<AtomicBool>,\n     │...\n 1205│    /// The planned-restart path (D3-3, Q3): a `BrainRestart::request` (the\n*1206│    /// broker's `KIND_BRAIN_RESTART` handler) makes the supervisor cycle the\n 1207│    /// brain and stamp the next spawn `Update` — distinguishing an intentional\n 1208│    /// update cycle from a crash (V2, broker-authoritative). A long-lived child\n 1209│    /// is used so the restart, not an exit, is what ends the cycle.\n## broker.rs\n   59│    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n  *60│    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n   61│    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n   62│    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n   63│    KIND_NET_SENT,\n     │...\n 2541│    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542│    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543│    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n 2544│    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n 2545│    /// `Daemon::run` after it spawns the supervisor.\n     │...\n 2704│    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n*2705│    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n 2706│    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707│    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n 2708│    /// reports `honored: false` (no supervisor to respawn the child).\n     │...\n 3190│                }\n*3191│                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192│                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193│                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194│            }\n     │...\n 3893│        let frame = Envelope::new(\n*3894│            KIND_BRAIN_RESTARTED,\n 3895│            serde_json::to_value(BrainRestarted { honored })\n 3896│                .expect(\"BrainRestarted serializes\"),\n 3897│        );\n## conn.rs\n  74│/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n *75│/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n  76│/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n  77│/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n  78│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n *79│static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n  80│\n  81│/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n  82│/// startup ([`init_log_anchor`], both broker and brain entries) so every\n    │...\n 141│struct Inner {\n*142│    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143│    id: u64,\n 144│    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145│    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    │...\n 229│    /// Render the once-per-conn retirement record — the F-039 token split\n*230│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233│    /// already-dead counterpart, the offline-peer dial churn) emits the\n*234│    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n 235│    /// SAME attribution fields — quieter class, never thinner (the retired\n 236│    /// class is what discriminates fresh-carrier churn from interleave\n 237│    /// artifact in the field). Behavior is identical either way: poison,\n    │...\n 242│            format!(\n*243│                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 244│                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 245│                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 246│                self.attribution(),\n    │...\n 254│            format!(\n*255│                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n 256│                 broker conn poisoned + retired, never reused (same retire behavior \\\n 257│                 as POISONED — only the failure class differs) \\\n 258│                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n    │...\n 272│        eprintln!(\n*273│            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n 274│            self.attribution()\n 275│        );\n 276│    }\n    │...\n 287│            // First poison of this conn: loud, once. The token is class-split\n*288│            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n*289│            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 290│            eprintln!(\n 291│                \"{}\",\n 292│                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n    │...\n 428│        let inner = Arc::new(Inner {\n*429│            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n 430│            label: Mutex::new(String::new()),\n 431│            first_write_logged: AtomicBool::new(false),\n 432│            gate: Mutex::new(Gate { half: Some(half) }),\n    │...\n 707│    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*708│    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n*709│    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 710│    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 711│    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n 712│    // monotonic stamps. Quieter class, never thinner.\n    │...\n 721│        assert!(\n*722│            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n 723│            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 724│        );\n 725│        let retired = conn\n    │...\n 728│        assert!(\n*729│            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n 730│            \"organic class emits the DISTINCT retired token: {retired}\"\n 731│        );\n 732│        assert!(\n## daemon.rs\n 187│        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188│        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189│        let broker_handle: Option<Arc<Broker>> =\n 190│            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191│                Ok(broker) => {\n    │...\n 315│        // makes an update a *planned* restart on this same path: the supervisor's\n*316│        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n 317│        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n 318│        // onto the new binary with `start-reason=update`. The stop flag is held\n 319│        // for symmetry, never raised (the daemon runs until killed). The brain\n## dispatch.rs\n 225│                    DispatchOutcome::Served(_) => {}\n*226│                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\n 227│                }\n 228│            });\n 229│        }\n## msg.rs\n 231│/// which the caller surfaces — it never silently no-ops a swapped binary.\n*232│pub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n*233│/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n 234│/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235│/// acceptance, not completion. `honored` is false when the broker has no\n 236│/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237│/// reached a broker that cannot itself respawn.\n*238│pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239│\n 240│/// `spawn` payload — the program + args + initial geometry. The broker spawns\n 241│/// it under a PTY (no shell interpretation; argv is passed through).\n    │...\n 871│\n*872│/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n 873│/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874│/// cycle the child; false when the broker has none wired (a bare test broker),\n 875│/// so the caller can tell a real planned restart from a mere accept.\n## nethost.rs\n 263│        sub.lifecycle_event(\n*264│            \"stream-sub-attach\",\n 265│            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n 266│        );\n 267│        self.subscriber = Some(Arc::clone(&sub));"},"isError":false,"timestamp":1784196034303}}
{"type":"message","id":"bb3fed04","parentId":"06db4aba","timestamp":"2026-07-16T10:01:16.906Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning test-only stream ring cap seam**\n\n**Designing e2e dispatcher ring cap test**\n\n**Refining claim state retry strategy**"},{"type":"thinking","thinking":"**Analyzing worker kill and PTY refresh issues**\n\n**Identifying session PTY and controller role gaps**\n\n**Clarifying PTY attach call origin**"},{"type":"toolCall","id":"call_8KZXIFNxMjR35YlkoRwv5Whx|fc_0698befac979932d016a58abeca48c819a864b05c428338e7d","name":"read","arguments":{"i":"Reading attach controller subscription","path":"crates/spt-daemon/src/attach.rs","selector":"330-455"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8413,"output":1056,"cacheRead":193024,"cacheWrite":0,"totalTokens":202493,"reasoningTokens":1015,"cost":{"input":0.042065000000000005,"output":0.03168,"cacheRead":0.096512,"cacheWrite":0,"total":0.170257}},"stopReason":"toolUse","timestamp":1784196034305,"responseId":"resp_0698befac979932d016a58abc2ea04819a98ebf1eb6bbde8c8","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58abc63cf4819abe6f9d0afa73cdbb","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvcz17K6iuwUIkQkhxGhXb-rwwgvLoGlFkUia8wVoGUQZtf3ASrTd17rTFiGZV9W4-wa_rAPmdxkB1b6JTxEiwjbWEBghWsAO14fw0DCqylp871l6gdtmRg03FrwaEnjN3NLIWpVuu05ynUqKLg0fkvgcU-kSv9t-5idY8dYdOO-2ZiixOIWMtdPHdfZySqr6tJqliZXb1Q91BV72bgEn3Zw_Fr5hF0r2AZ3qU9XxJBOhKFo3wckcMixElR8lUCQDqg3ozNdgz_kK7CSci9AnQjAM3BV7RjG1zsITcdhgWcL6uH71lhsoMSJ-yFTPoHXzx2_Yrr2BrNce_dOvELpnHFQhmAF_blezfD0Q-97F1f2mTSPviM-nwrzyevP4MROLzXOVCF1I6oBWNSZB6P1mEK5lFjLvztE-f1CeSZHTEhwCaXDpqsnlVmJCB2jfObZ1zOFGdK2lsQZSikz1wZuQ80lSfzoA9yvmFMqMueE1AMvqjuEn3I1nKcPWF06WbI1b38Chj2kvbHMaelLFxdg7SfzOVy3q2HqeZVHkWep6En1O9T0Npi-HMG7e3DNoh-uZpJf6m_e_08Q18-GyqSkqLdLjrxWW723WnnQhPS9bftXouotM0tsYzDRghZoYBROo8ahTuIwVqP5mdtkkNN6adHfs0OeD9MX-ekLMCRwJ_KcwHXEJaEmcDexEMGbB6VRhyAta-mh5kIYiveeNJRRhBxDcevNXxFqPPfjMumLzNOhxSlylPjHAwaxgGoAMguNM_se1DzU9JjpwIzgAXnWTH5WXhEj0oP99Bloa0fKcZu3czS77-YB0-M_bp8Hv5z9fxIgZyQ2q0N9CputJm6pq8p8rktiByiiYPpSBCSurK0HhRdDNfHOuw0wo8-BvUL91NpufeDtRyyFMJKT0KERnWnTchFBkMFk8LCPkqhPgOovBESO5-sLEV50itr1Vk7P22jR5tl2fwIewpho0gQ4v8HEWMr7kRsrWSbMS4eqH02dwoQwHDv6o4HSBemj3x3cyLFAmmf3IA-9Bad8dn6VtJtIo8-8FeEt8Ekpo4p7dr-fgSWy48yuEV5dk-hPhevw7dJYo4KXIoy_BujQiqwr9tjtR8y8WaD9LakX-RRMja2N3hemlPdloUEMzPZoUr27x2K4OBUO8liUgHrScpHrmBF9zl2Nu-XE6CZ3-FZ2hd5zj_Ss5oZBzjDedClRkhClpsuf9-NKwy3O2wtPlEIGARFMb8mmZO9kl9GIhZIomXDe0mZYVjWba7u_cpdd11u_OUpWeXme6EZnwSs3m_By19VUHI0ovvbmbxy1pKBy-iU7UIGoqM4XKJ3gwCgqfI5Xmm-lGcJFK5WW_deizdrig7xCsnll2vay_qw1m0tcMLaNjwmGMLzWgyH7xB_yLb8yETpjVLf_zVN9tmPKDdpXO_E0h8f62Tc2btMKmLKVP1KlMNh3hSDwAbA1WxkbdIx3hZyU77YERP1QPJRa3Hp_90YXcItRiybgkoMXxEZxEhohWC9bO9ZRz992SMgfE64L3HGw4SV88BYsrAmoyNh6BXW0Z78zM8enQo5Nlxvo4h5F_imsvc2oEds-ocuL0a1WnrsomIz5oQ4bFrFs56-wX-_y0Kmz0Qd-h7ElHjwpOthP55LuQ9NgFKMhLAdBoHq5DCow5_nA7Kc1xJEvnoYTdT-FjG25Bg8rDsFy091HCaC5t__1yB3kkUcy2D2MxXlNHs_eEc91OOTK4SJkeljxiZQE5yelRj986ct73_07aHLxhRwuBVdLgx0GCeE-7WpnsEKbSXTM7asD_k7o0gYKBeU3aIPAhIN__r0SKfRHJ8txxq2WNRb0fgtjJEZ5CgKUNxAGqaA49zhUoF9bn-FpsmjRA490IQYdovnPovKNAqd5y5q-8CXLLns0ABgDVfrOr18t3X6jlFPcOsiHWT3FqVPqml__wlOntO8Nx8TIqlcyRq75AeErIxRVDTUt3Q5d3T72JrXOqIGzWXfCamZ6cHHAaAJiPoaNxTuyJTw3a_2PLDpX9ltLFPeSk55lww0mturgqEU3OmPcUxBHSs53ScuYb_hnytj1a7yJIIx-DnH0UuRiASl00pfFWvGfbTLi6Sj6fXEdjeQPGOXGX4RZYO6b8_OTD-GEniOnoFsJ-SHl-4iQtcmwln650EBlJcYvdTFP6_G9YFda2qvdFqBc5rPCpSoWfP3BIETn1G2w0Bz9nzbBmuZrSg7Mc5iBV5btvqj3rvSIXEvbnN_MyC35h505WC-ENhCN46GYVu4nnMwkPZ9vQpJlxM63AfLtDg0t19_Z3ZTNkq-GFc0zdelD3Y18AnCIkkRH2v-VBUF4EapxSVZnDQmj2crqDn8p_RW-NL4PBUcFzRVUpT2m88ENakvtUHM9qCrE3Kf557I8U5PXUdTp1fJg-hedu3Jd-QpYT3lPtotTZ8CpwX7gTCQkBBNNlwpneSzMj8NO20iKnl-6fIVK1dN2mJbOWYqA3amFz438zvqcR8y6CTHR0oBjIB-1kMZbdhjHOGXPOEusZKXZ6BpTFpTnIcIE0l7SYfBQzt24t8zdTmeDK6E9ePrlUNsru_gDMG_LARsTQpDRiMwgBp_yVHs9Ldx6GTWJbdG-6RQJUXsaB047cVZ4dH2k34mplmnPAxxBhffE5K70Zm79RBci-_vsJQ-gy8UMLud9RJVCM6sT_CbyCPW3QGA--mkxqWBI2PrFLmL07CCDBXSJ8SDuS3idSZmuR4CmIhQwn6lRqW-fGmxSxiT5m2Nyw35_a1tTTerXbuO7xGuu_ViBDwiEBUN95uKjniyD8NrGHf2-u69NSsfdd1DbqBxe2X0IZZ-QvaSEmmeuo372D2zdPm1n7c0JyYxFJJeMVaE0r9Tudxo5ANP4K8S7K--ZDxULR_mxsTolQUA6AlHXwuUnWvEvluBIgJg-_sf1r-RTVdJrAug6SnWMUrzbA7zh-_vP_nMExat2iWZvraxegunEYJdfCM2rtgUbsveyRBLaoXwViC99d7aKCAgdg3jSTVc_uCuai0S7sBz6CXGmEp1CF9b-JOIYI4Zn8-nr3jT2LgkgJG3Cw9jCDZ2Z6nD030azISblIglBBME1QCLWneyvbvqqZi-9CJ-PuPnTkAyzNS-e_T5M3p1rQ9HwHF_IvoVMix9ZoqxTqwRWc476K1NIgCZtxbZBKNd_HNi39oKlDejhorYHtkwYrYf6jQIqLe56g4utFGS_wIOMtQ_HpI1pzZds3z3NNdk6TUJodUdMG5gCMxEFFyaaYFunggOQ70boXH6-kjV3YbVsqY-Le-SRsB1a2DXArSqj_-iO_ds_Ziy5AbJp2qAs8_gwI8bPIWJKQsitq8-yLoYOpgCn1mRqBe0n3RqZeeHoXJbLz3OHsAq6HfesKHvJ7OVxtlBOl-Uk_ZhDwz7vAV0n71PTx6W6KrDH3Bjy391EEbrM65t20vhDdoaGA9Qtee8L2EQxG-xY87xHX29PKI30nZ388Y_lpFfQ-MPZ_kZBgW7y8w84hv3lE_Jz41wxjVHv-V2JI-SzModJcuIxHYlNaHl9i6twefoVgvrYc_3o-EBMP4iz0wvObmgVTMrPfNfCRv9yuoZMM1W5PToWCFathJ0xgFV3zs32KLhu9MJxuzj0p74WejHPC7JBLa1_JLnNPmnnUJl0n38JaHyzuOwDD3aPclbeJnPLn0b8gVNnFBEocUfZh4ByPoLhJSnuk0-YoJTgTjjh7XqjpsSm98A6bM7r2nYRLLoIQa6hhFyfecO4XqyDgTGYhjAGY3MZ54bR_YiwuocPwltYJC2fCF6SMRthvPCuEeJo8W6RtYWgrK3QUyhuvHY0MYcJA-wnBjC4N1HkrDgHd7lHl01aEz-COKX-DVUaPvmh_Esb-A4hFvNBMwwQJInKScaEk_XpUEN4FbZpb83PtqRczNyBruqdebBGutcNB91EaUvmGPTadnhV-z-jBRZMWA4cKdDFvskT9fGYkLgTVRnqLl_-i1ELrc43leVvX1KlgkiLhC1QdralBrevi4dYrcKNzUt9BfLdDAJs2BPTe8jE7ATpW_f2QG4FhLfzTM-16z2JUnvAhZTUwFlNkJ0eBRXlRQznXcZNOliAW4iltpz_o8hYa1W-LMLuUX6lkGZC-wUl5mq4JtLsXLRIk7I9Uky-We6nCB0TGk9DHVSxJ7b9nAUtL9ypN209dnluWsLu5wcp9_RgRm1zoHAX1xltFpJDxnt9JUY77-aMT6etG0Aa5mxiTcYQN_vigQDlg4RLqTFJHziXfPrkF0Ep87N6Fqggda4NDL7pKHB-lnJseoDw2YNUcZ4W75t8o8y4FW6nu8hiew0_Exmvi2GyLkOds_50hsPpLb5aZ5GiO6qHGPbfMm88FYjTTgxNUiIlHk=","summary":[{"type":"summary_text","text":"**Planning test-only stream ring cap seam**"},{"type":"summary_text","text":"**Designing e2e dispatcher ring cap test**"},{"type":"summary_text","text":"**Refining claim state retry strategy**"}]},{"id":"rs_0698befac979932d016a58abdc5f04819a83f11e4d6a1dbf07","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvshSKhsUaiYq_GxzJLlbQR2GD9El0Exlj01doro_XZXk2gczMN41EICjwGPnCPRYgCxdBjGper8AC989b-dmAl6_udQrTm9-jQczdgqZdN8Rt7bIAGRz0VMryzbjfhPKEfnkz7pvj1Obe975UJh30ixm8pO9dqtkWFO2rNG2vhtY_pb29u3kzP-3qK4bKR7mJEA3GiPnEOg1I0fPWAZmXC3fLLhi1GSCLF4eb3L83o52Jn6M2hkzvn-qs5qY32wF5lBp7t7OYlCa36e7Ekfg9jjlBqIGh5Y0087wgTaHWYjNXb2NhDcnP98sINC92SDLK_IZdyB0GPkAtuKaremENUki-9BQVaY4yIPcUMSP0qUdz8h7Joi0LR5GoPYvHrOebeC0fy2TCAzPEReUzKtPHK1ddOqpnkggOBryorBJ6fWktVfQki720FqZW2dmcALyjLWIo1FT-U3U23YmMdnUYfl52pZSaHascG3_HMrxQk8jPoh7-Y7WHAom7vyxu_NZ_-gb6YkewLn2A93ShtyqqQFRaT8ie8tNX89tP99Zyob0IPxKxIL9ddWU3o29ouh64F7GPsAGdlKJNF6MbD-0YZNGLnuo_mi013V3Omr1dTOI0elDITv0bRADC-v5C7hFndcpDUvLrY5TSBPg1_yt3IdR5SehcZW_XA5GnlP5d4uK6cU4cn3KJWQorkmCDoMdyMneoyJYiIaBsFccN1HsduJLLWG2ljFbW-OiT04U8-WsdvVAgseylpokgwtXxAAxxIuct0rQ_Q1gmldhm32vytpHskM5rLf2-rh6WYZ9KpADqxN4yx9CDo2vJKX_kCJyvt419QTCay-a4A_FK77oohLOOoEx4-eEsSAwCWWq1fsu8eXQIjuJIv76FHQwFL9U32IdTMpxOsfZETV96vH8PItThS98g3yEPmoZYkyYn8i9-O4GBXavEjK29QnyNfPEK5tdL1w0_VQo9A9KlNth6rMvKEA4YxtSfrw6yGKTgKsVWaUBRVQXVJBlN_udqZ2YhycvRt6w5xyShmh3__DR7rVA7ju8YOqJI3GCSPZujuTtTEVl_I-vNyNpxEtpZZBLXNqSfeNlV1sAL_unxuOKhktfn2Vq1c8496M5vI3UrkJQtdo5ldzBmTWuK1a8t67LWuBYojPRmdlqX6PddfU0UlkpGbwSbK4r-rI2vYipLxB8AFT4up1eEJdljsYeNx4vJC-t4JhMZX0RUlJUYCWXTujSj35FDBFsdvdW8RjXusJIV8uwrSrXFv34MB2-wJZxstJlqK6Xu-eO8ZVZftYKhXwC-lyCNtAFNZx7Gsn2vDsiQ4sphpVN0Z1Y0e5TNHgT5Y97LfgB81MY4_QUBAMDpmbiDJ4IXIcym1WLcWYVoXSxtwsNDle0lvcR3Qqpkx8e0tSaW345qYPWqTL96vnuS8E3hdUFlPtANjiGnK_wUop2UtWRVCnyuTD7fGJBxXGbzJ3fbCio97tjr76GPSpAWR5b6m1_owdZqUCO1Rkpg8IkQtqP9BcvuDD0EYLGnm74kLZjQG2wCuxnF4RoQISNpUvRep7SLCdqyctR4QyxqduaPIJsCXI2XYtMhe_fAYO9is_gmA9oTRiht0xV-pu2IqF87xtv75R4hhDcYHhDBuP_blg9Lww3Nxfda2mfDihqt45OYBlz2F38CRHE7O53IWxmYE7vDWjdmwlSLhWQnWwoVYetPXlu4VDCDKDUcC8a2ZU2Rd6F7KUKyxlRCJrNPmEiJm63Sq7mPWTSjSRpYFbuzfVjen1C3QkIwApmdeBsTVDKRRSAiYyFgPYCBWRU1rJfJkfg1ILlKC9SBKAWY3z8EZydh-aniHaz0vBfrle5O4G6Iv-7q9S8Um07Ny1R7CJXcL48YHHnUlgH8INnErATzYFyKyy1L0QIP7NRvQcRhmQKzEJz_9odhCstWo5wmC31Am9B_wRv0sPHm2drLi8K2rJkGF0rcjHxb1VsXen1neCs3GaK89cBiZY1busA525DP3w_-ouKMJ1zbmYYgNipFQjkduybb7cYQQq7mxq4RPVlubz_x1MvdonJoGjsKgCoNpLJhaTtY1ZchL_FJ1Y04oXWeS3uxHRElfyxPV8Q2mM2cPUn6GfTf5dOMVdMwRtj-r-YbbxwEte43LbSsB3B03nckXSI5IWe_lji9gGOTHp8VOv1CLZRmr94ng5-u7WT34D538zxgYCCxLzrnvHBYvrod6xtOL_mq7uIF0F3iOQfzIFDzVCNVlvB7ALh50xPqNggMoiuHggp3iGJOVSG59gphDSV6MFFGbMrnFaGzn23lmYFEwfwI9UZ1WuHLKjmFp0JJhKjHRsBqrYbNOupGf-8zXevTsCDid4osuJWOJ18E5mbovw9vDP1ei6QamlzW1dNp-ZWf-ynSVA5AbSjwk3x9AP-OQxeygo9Ke6vJ_Jy8SqIvVrb9isRQXmDB1FKa4gSiV5s5WaUsjtYOrbA0IuTULvQZgzOc50U0d8xWoofEbekhigzH0pBKmCNVs0MafO7mrcDHV3nhF0UCrHBe_i5h_3fv-G945knVeX1s0bi-LXPlCHnbeRnLUffEprfiVR2L6U9cR4b_PJseL74ID5g7Eus_6-i1TH95UKmvAui-kXufpjxgrllPbbWgOw4rZFD-s7OTbuM8rNY6fzwi-pDn4wHG22uOpZ4Z0lMl-ztr-MAzC8EfuXn-oETbWeFgTYoNQYjJ8eeTIowdDFVKhOi6hjidAc-u4-Wa82_8ZNemzBsWzvU_0axEncCYYIezqyUEb7wezJyjcN7cfUcxOUTRUaB1JGA5ikI0noDh0QdYfMX3pSVf7SvMhjdMJOG9zVzlyxlSj1nzglg0Q5b9IEFENRSM9L3S2DejRvnzKySGDxwKf-FNcHtMBVkdzLcVyMEnMh5MP0QMI5B8av_PLz_9coUIoLMAo2vP1VYE1OL0FfC8Piki29kmcCS45xKD4wn8c2NWkNGF_wHY-cDR3EH4NGRcASaHUEmWaVdNu3EC1E5GlXNWu7FkrcqlRalx_4qZaJrMuicYmcsM8eCVpP92h6DqgtRuBpXnSdKRKA3WOOR_EwGnuUFZ44laZeUF6uLtoSbwd1sYuzb2CRGqsVvJLbqRGdEZCc2tzXx06URxYJO-GgOzTuQrDuG-lmIMVvtNXKEYkWXThgIytg5bfp6eZ_EqzKPQ42qrEH59pFVEpq_JoiMV9Wr951KdIDKmssZmDawj82Rzy9XvGFPbuCpstqDBt1vaklpQ9TIndfkbdZT3oW4PVxgwmQzF1IKXHKXykClwNApfQvnyOuMyNIgfygksezR5TEC7qlknkDhQP4AcCI_vX8El4N_sxnJmPDn5G0v29hU3cxz9GPnNNbFbvNeO4OQO3sb7cM1pRuNv6hch1wDTRHga7tNz2qXrdy6cka0649GX9ei5ZDk6sr-Dp4JJr8Fqv1dob9jKKzXpLeFeoNWzbGDA_fwi0g8UpIGTGJsSScFuu8fJnK_i_BpG8jgIP-eAKM2sUY0SfpH15K5ekbPV5pNbQhThgn1wce8xYR-L0ElEtq5Q5OOliUJMOd3bU43dohIuYIDSz7AVB0mxgQ9NJmOG7qY269PhYyKuPPgZ8X-0Wg8n54_SY6cxYn24gnPBFgWL6jPDeJTE4g-IeiytYRRAZWwN3UYElQNPtJVoThi3o7eS8y0RKehSjehrK42y64PGGfUhvpGVT32ebuTZYJZpFbvSMWRV_zhGl3nHo-_4X5TBBkk3ucroM-7OPLmYlbasCj4AZWoyMW4jQPs-MYtcyZ3e5p0mdcG0OzAvKFMibKSa-AqAbI7Lb3K-u0Qj62BWajp-ViSfhi-N6K3leq2QeVrYakgyYC8NgHKZ-FPHTJO4RqD2zWz2KR29Vh48fG7uWYcJzMRM_Ejc_nBi3hR7Y_k6xvs42hiug-Zfma6QaePTGIvhWGNsIa6MloUDIyJx-Yt50Ibn-un9f9ytiHkc0t6HDrSfVaVSfBf9h8wx-Ls9aaXSqwu90RIJbSxJgDzktM-Mqj-u1xofMqlJNRP9OAPyDxQ==","summary":[{"type":"summary_text","text":"**Analyzing worker kill and PTY refresh issues**"},{"type":"summary_text","text":"**Identifying session PTY and controller role gaps**"},{"type":"summary_text","text":"**Clarifying PTY attach call origin**"}]},{"id":"fc_0698befac979932d016a58abeca48c819a864b05c428338e7d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach controller subscription\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"330-455\"}","call_id":"call_8KZXIFNxMjR35YlkoRwv5Whx","name":"read"}]},"duration":42600.235699999146,"ttft":4044.6008999999613,"contextSnapshot":{"promptTokens":201437,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8KZXIFNxMjR35YlkoRwv5Whx|fc_0698befac979932d016a58abeca48c819a864b05c428338e7d","toolName":"read","startedAt":"2026-07-16T10:01:16.906Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach controller subscription"},"id":"70818a42","parentId":"bb3fed04","timestamp":"2026-07-16T10:01:16.906Z"}
{"type":"message","id":"8c6cf7c4","parentId":"70818a42","timestamp":"2026-07-16T10:01:16.920Z","message":{"role":"toolResult","toolCallId":"call_8KZXIFNxMjR35YlkoRwv5Whx|fc_0698befac979932d016a58abeca48c819a864b05c428338e7d","toolName":"read","content":[{"type":"text","text":"            BrokerEvent::NetStreamData {\n…\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\n                            // the transport-proven origin. Viewing is gated\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\n                            // watching reveals full session contents).\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\n                                == AccessDecision::Deny\n                            {\n                                wire.net_stream_send(stream_id, &[], None, true)?;\n                                return Ok(AttachServeOutcome::Refused);\n                            }\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\n                            // dispatcher-resolved session id (the owning node is\n                            // authoritative for its own table); the LOCAL path uses\n                            // the wire session_id unchanged.\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\n                            let sid = remote_session.unwrap_or(session_id);\n                            intent = req_intent;\n                            attached_session = Some(sid);\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\n                            // resolves controller / viewer / busy / take by the\n                            // intent + this handshake-proven origin, and replies a\n                            // `Subscribed`. driven_by / viewer_count are now\n                            // BROKER-owned (single writer — resolves the\n                            // clear-race), so this loop no longer stamps them.\n                            // A VIEWER attach ARMS snap-above at the floor so a\n                            // PRE-eviction ring-roll forward Output gap (the ring\n                            // rolled frames out under flood with NO eviction\n                            // marker) SNAPS to live instead of fataling the\n                            // legacy reject-gap → the viewer keeps forwarding.\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\n                            // resume, B2). Composes with the POST-eviction\n                            // ViewerEvicted skip-to-live arm below.\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n                            if req_intent == AttachIntent::Viewer {\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\n                            } else {\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n                            }\n                            attached = true;\n                        }\n                        AttachRecord::Input { data_b64, op_id }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            match role {\n                                // Confirmed controller — exactly-once at the PTY\n                                // (the operator-minted id rides the B5 journal). NO-ACK\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\n                                // fire-and-forward (the ack is discarded here) and can\n                                // flood N input frames on this one conn; an acked write\n                                // back would fill the return direction and deadlock the\n                                // broker's per-conn handler. Dedup still holds at the\n                                // broker (op_id applied-set).\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n                                Some(ServeRole::Controller) => {\n                                    // The op_id is operator-minted (rc attach OpMinter),\n                                    // so it carries the `rc` journal tag (ADR-0034).\n                                    brain.send_effect_no_ack(\n                                        MintedOp::new(Minter::Rc, op_id),\n                                        &bytes,\n                                    )?\n                                }\n                                // Outcome not yet known — buffer; flushed on a\n                                // controller confirmation, discarded on busy/viewer.\n                                None => pending_input.push((op_id, bytes)),\n                                // A viewer never reaches here (intent gate).\n                                Some(ServeRole::Viewer) => {}\n                            }\n                        }\n                        AttachRecord::Resize { rows, cols }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\n                            // broker rejects it on a viewer's stream). A viewer's\n                            // Resize is dropped here by the intent gate too.\n                            let _ = brain.resize(rows, cols);\n                        }\n                        // Input/Resize before Request, a viewer's input/resize,\n                        // or target-direction records echoed back: noise — ignore.\n                        _ => {}\n                    }\n                }\n            }\n\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\n            BrokerEvent::Subscribed { outcome, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\n                        // Flush any input that raced ahead of the reply (operator-\n                        // minted ops carry the `rc` tag).\n                        for (op, b) in pending_input.drain(..) {\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\n                        }\n                    }\n                    O::Viewer => {\n                        role = Some(ServeRole::Viewer);\n                        // Wake-neutral: a viewer never wakes the endpoint, never\n                        // keeps it awake (Gap B). Discard any racing input.\n                        pending_input.clear();\n                    }\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n\n[Showing lines 329-458 of 627. Use :459 to continue]"}],"details":{"truncation":{"content":"                ..\r\n            } if sid == stream_id => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r","truncated":true,"truncatedBy":"lines","totalLines":299,"totalBytes":8301,"outputLines":130,"outputBytes":8301,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"            BrokerEvent::NetStreamData {\n…\n                ..\r\n            } if sid == stream_id => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r","startLine":326,"lineNumbers":[326,null,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458]},"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":8301,"outputLines":130,"outputBytes":8301,"shownRange":{"start":329,"end":458},"nextOffset":459}}},"isError":false,"timestamp":1784196076919}}
{"type":"message","id":"de63c11e","parentId":"8c6cf7c4","timestamp":"2026-07-16T10:01:25.942Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Searching dispatch_subscribe usage**"},{"type":"toolCall","id":"call_GlRIN9JagMdxQrbUCtTt149f|fc_0698befac979932d016a58abf33ca0819a964972e129245b35","name":"grep","arguments":{"i":"Finding controller connection attribution","pattern":"controller endpoint|role=controller|describe\\(&format!.*session|dispatch_subscribe|resolve_subscribe","path":"crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4734,"output":94,"cacheRead":199168,"cacheWrite":0,"totalTokens":203996,"reasoningTokens":16,"cost":{"input":0.023670000000000004,"output":0.00282,"cacheRead":0.09958399999999999,"cacheWrite":0,"total":0.126074}},"stopReason":"toolUse","timestamp":1784196076953,"responseId":"resp_0698befac979932d016a58abed5270819aa42c82cba63aa7fa","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58abf228c0819abec49ef31f0c5c80","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvzl0y5gpbzRlS9LvaWtnNkiOV9CQFRimwfc9_3LHTl2HtgAKHxHTVKC2tiTIkAH9KJZ2E2xNEq_7cXXuweZEKsVHbVv97kAIWZHSWju1VgT0NhdbGWKepfWwotSKHs96az9mKV1g6qevXI2UdRHeX2tAMEeamuKjZzcw2CD_kZH1fZZy-1DNGNl7SzEYmbjQjL9yLDeDsHmPcT-FZLpvWt9TxmzTfNQE21THe2BEuij-EnqoHoLOTKaoCqs63zXoZC82QhoyoNstdlx9bhwejUOo0P1LHzeminsmvvjUi3LEFDQ7GnpnQjJzLZ49WlXIu6c3QbzdB0XhrDS-pZE2QCZKERtPREByfGZfW8mFMH2E29Dc0jWIvXCTkx9Cy3bnaivSKvVKlLRmKqIjehXeF2bUf1mh7a6CohedzWrC3a9YEVbfL7FiISjzpMNXVwZJgvqK7H-h9baqqMPkM8GuY5kGJ4tXhmTRRXJTQpTNfpu1KIBxLASiGK5w5B8W_QGfjozGcYiCPf-4iWSmJIs_ZxDAQfL7PH7p4mEd-OxVb-2P4nxlEukh87MFYzoDrzW3AbiUpXy9pTUC-PtrtW3P2X-CmQ4PZ3GVoL_mZPMwbTgQiO4EMq_4zKWvsy_mLmN1t3aQ0M-OT6GrU27jz7nDxlmM21J3U3AxdWUwF7vsJNzCmnr-jnVLZZ71shQxmKYUQ9rqJRCAJMTJrQ_dMfCefPi2R8IMq7wOHpKP8mvTYvB7gNbUvSzHrx8S58LGQxFY9v_iVoQF58cChFO_BwjtxbfqmEoT3byIwjog1ucFsy96F5A2WPurFm8w6wBcIer9lGYFTiw4dl0hiYREUI1ea2c8Xbhh3JGaubj2qzaGkdYrCqZmJoFTSFCWhHi3g6hX0s76edAcc4a-SOIBUHZwNrWsvP7NkrGGuboCzQggH2lY8JRtuDsEJq7YTlV74WYGU9OEnoqOKoQFowsqckZGejk7xrbQDPEnFhERgQKAYADMJaC2hlwFUo-xP1XdhK48qx","summary":[{"type":"summary_text","text":"**Searching dispatch_subscribe usage**"}]},{"id":"fc_0698befac979932d016a58abf33ca0819a964972e129245b35","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding controller connection attribution\",\"pattern\":\"controller endpoint|role=controller|describe\\\\(&format!.*session|dispatch_subscribe|resolve_subscribe\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_GlRIN9JagMdxQrbUCtTt149f","name":"grep"}]},"duration":8989.107200000435,"ttft":5184.216599999927,"contextSnapshot":{"promptTokens":203902,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GlRIN9JagMdxQrbUCtTt149f|fc_0698befac979932d016a58abf33ca0819a964972e129245b35","toolName":"grep","startedAt":"2026-07-16T10:01:25.942Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Finding controller connection attribution"},"id":"c868eb1c","parentId":"de63c11e","timestamp":"2026-07-16T10:01:25.942Z"}
{"type":"message","id":"f5949955","parentId":"c868eb1c","timestamp":"2026-07-16T10:01:25.946Z","message":{"role":"toolResult","toolCallId":"call_GlRIN9JagMdxQrbUCtTt149f|fc_0698befac979932d016a58abf33ca0819a964972e129245b35","toolName":"grep","content":[{"type":"text","text":" 858|    /// or `None` when undriven. The `by`-keyed ownership logic in\n*859|    /// [`resolve_subscribe`] reads this.\n 860|    fn controller_by(&self) -> Option<&str> {\n 861|        self.controller.as_ref().and_then(|c| c.by.as_deref())\n 862|    }\n...\n 1147|    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n*1148|    fn resolve_subscribe(\n 1149|        &mut self,\n 1150|        sub: SharedSend,\n 1151|        from_seq: u64,\n...\n 1557|                    \"writer-exit\",\n*1558|                    &format!(\"role=controller session={sid} reason=superseded\"),\n 1559|                );\n 1560|                return;\n 1561|            }\n...\n 1565|                    \"writer-exit\",\n*1566|                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1567|                );\n 1568|                return;\n 1569|            }\n...\n 1596|                    \"writer-exit\",\n*1597|                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1598|                );\n 1599|                return;\n 1600|            }\n...\n 1614|        \"writer-exit\",\n*1615|        &format!(\"role=controller session={sid} reason=channel-closed\"),\n 1616|    );\n 1617|}\n 1618|\n...\n 3020|                },\n*3021|                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n 3022|                    Ok(id) => my_subs.push(id),\n 3023|                    Err(msg) => send_error(&send, &msg),\n 3024|                },\n...\n 3103|                    // lock, then read each log's cursor under the log lock alone —\n*3104|                    // never both locks at once (the dispatch_subscribe discipline).\n 3105|                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n 3106|                        let sessions = recover(&self.sessions);\n 3107|                        sessions\n...\n 3448|    /// self-kick guard.\n*3449|    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3450|        let req: SubscribeReq = serde_json::from_value(env.payload)\n 3451|            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n 3452|        // Clone the log Arc out from under the sessions lock, then resolve under\n...\n 3470|        };\n*3471|        let outcome = recover_log(&log).resolve_subscribe(\n 3472|            Arc::clone(send),\n 3473|            req.from_seq,\n 3474|            req.intent,\n...\n 3519|        // under the sessions lock so the ENQUEUE happens without holding the table\n*3520|        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\n 3521|        // dispatch thread is never parked here even when the harness has stalled\n 3522|        // (P0 / REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\n 3523|        let (input, translation) = {\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":12,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":12}],"truncated":false,"displayContent":"  858│    /// or `None` when undriven. The `by`-keyed ownership logic in\n *859│    /// [`resolve_subscribe`] reads this.\n  860│    fn controller_by(&self) -> Option<&str> {\n  861│        self.controller.as_ref().and_then(|c| c.by.as_deref())\n  862│    }\n     │...\n 1147│    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n*1148│    fn resolve_subscribe(\n 1149│        &mut self,\n 1150│        sub: SharedSend,\n 1151│        from_seq: u64,\n     │...\n 1557│                    \"writer-exit\",\n*1558│                    &format!(\"role=controller session={sid} reason=superseded\"),\n 1559│                );\n 1560│                return;\n 1561│            }\n     │...\n 1565│                    \"writer-exit\",\n*1566│                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1567│                );\n 1568│                return;\n 1569│            }\n     │...\n 1596│                    \"writer-exit\",\n*1597│                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1598│                );\n 1599│                return;\n 1600│            }\n     │...\n 1614│        \"writer-exit\",\n*1615│        &format!(\"role=controller session={sid} reason=channel-closed\"),\n 1616│    );\n 1617│}\n 1618│\n     │...\n 3020│                },\n*3021│                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n 3022│                    Ok(id) => my_subs.push(id),\n 3023│                    Err(msg) => send_error(&send, &msg),\n 3024│                },\n     │...\n 3103│                    // lock, then read each log's cursor under the log lock alone —\n*3104│                    // never both locks at once (the dispatch_subscribe discipline).\n 3105│                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n 3106│                        let sessions = recover(&self.sessions);\n 3107│                        sessions\n     │...\n 3448│    /// self-kick guard.\n*3449│    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3450│        let req: SubscribeReq = serde_json::from_value(env.payload)\n 3451│            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n 3452│        // Clone the log Arc out from under the sessions lock, then resolve under\n     │...\n 3470│        };\n*3471│        let outcome = recover_log(&log).resolve_subscribe(\n 3472│            Arc::clone(send),\n 3473│            req.from_seq,\n 3474│            req.intent,\n     │...\n 3519│        // under the sessions lock so the ENQUEUE happens without holding the table\n*3520│        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\n 3521│        // dispatch thread is never parked here even when the harness has stalled\n 3522│        // (P0 / REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\n 3523│        let (input, translation) = {\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":1784196085946}}
{"type":"message","id":"68a12ba4","parentId":"f5949955","timestamp":"2026-07-16T10:01:32.049Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_HyXThuG1HuOvVQflL4HfJ7nS|fc_0698befac979932d016a58abfa46d8819a85ec55c6e01de3ef","name":"read","arguments":{"i":"Reading controller attribution and retake","path":"crates/spt-daemon/src/broker.rs","selector":"1125-1215,3440-3495"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3614,"output":49,"cacheRead":201216,"cacheWrite":0,"totalTokens":204879,"cost":{"input":0.018070000000000003,"output":0.00147,"cacheRead":0.10060799999999999,"cacheWrite":0,"total":0.12014799999999999}},"stopReason":"toolUse","timestamp":1784196085954,"responseId":"resp_0698befac979932d016a58abf6723c819a92e9b49ea0605ede","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0698befac979932d016a58abfa46d8819a85ec55c6e01de3ef","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controller attribution and retake\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1125-1215,3440-3495\"}","call_id":"call_HyXThuG1HuOvVQflL4HfJ7nS","name":"read"}]},"duration":6094.433299999684,"ttft":4314.791500000283,"contextSnapshot":{"promptTokens":204830,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HyXThuG1HuOvVQflL4HfJ7nS|fc_0698befac979932d016a58abfa46d8819a85ec55c6e01de3ef","toolName":"read","startedAt":"2026-07-16T10:01:32.049Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading controller attribution and retake"},"id":"1a496d95","parentId":"68a12ba4","timestamp":"2026-07-16T10:01:32.049Z"}
{"type":"message","id":"bc3087b9","parentId":"1a496d95","timestamp":"2026-07-16T10:01:32.089Z","message":{"role":"toolResult","toolCallId":"call_HyXThuG1HuOvVQflL4HfJ7nS|fc_0698befac979932d016a58abfa46d8819a85ec55c6e01de3ef","toolName":"read","content":[{"type":"text","text":"    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n…\n        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n                    let _ = old.tx.try_send(CtrlMsg::Control(frame));\n                }\n                self.become_controller(sub, by, from_seq);\n                SubscribeOutcome::TookControl\n            }\n…\n        }\n    }\n…\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n…\n                }\n            }\n        };\n…\n    }"}],"details":{"displayContent":{"text":"    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n…\n        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n                    let _ = old.tx.try_send(CtrlMsg::Control(frame));\n                }\n                self.become_controller(sub, by, from_seq);\n                SubscribeOutcome::TookControl\n            }\n…\n        }\n    }\n…\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n…\n                }\n            }\n        };\n…\n    }","startLine":1096,"lineNumbers":[1096,null,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,null,1220,1221,null,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,null,3497,3498,3499,null,3502]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784196092088}}
{"type":"message","id":"67db9973","parentId":"bc3087b9","timestamp":"2026-07-16T10:01:41.224Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning controller line identification**"},{"type":"toolCall","id":"call_40YUs4YmkN7JmtgBm0WGg5YA|fc_0698befac979932d016a58ac02f5ac819ab01cf153f2043cc8","name":"grep","arguments":{"i":"Locating controller label assignment","pattern":"sub.describe|writer-start|role=controller","path":"crates/spt-daemon/src/broker.rs","selector":"650-820,1200-1650","case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4414,"output":89,"cacheRead":202240,"cacheWrite":0,"totalTokens":206743,"reasoningTokens":15,"cost":{"input":0.022070000000000003,"output":0.00267,"cacheRead":0.10112,"cacheWrite":0,"total":0.12586}},"stopReason":"toolUse","timestamp":1784196092130,"responseId":"resp_0698befac979932d016a58abfcbdc4819abe5a1029a3ec7f00","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ac003fc4819aa17165f83ebd09af","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwCnjO_4nPwGFO8h9WAO0QKsaZHcjS9sE-V96xt9nE-pQS_UtCbZAH3k6nEzvSREHcn7Qpru8iB3LjP0aJ2-9DmfxiSz0qk7oxP4fTgGZvYeP2zHtQxBCeLvCa387Tqx0vd_82hkkIUCu6EM13zRHbw1ANVayIjqca39k0NDz5hcf8NE7zzf_XkFQu8ctbFakrKxjhYy_fSeYhUA4RzWKNk0EEna9gSjFxE9KLiSGQ5GY35TCBPmeiVjv91vaXvG1l76pQwJFVgL1eyrAglWLQuXq7ZtOlQru_YtxF4hu7esAIyK6_RxWveesh3gBseGyWkepLmcS-W3IL4jfqJLKMCf9C8_CpUvD3ooESppT-BTuJkUXj7ke815Esuckvcaylxd06NMDv-5HWevGc30Dj2a8hly_vvpf41h7Oe3Xer4Od-3zwZZihJ04qenQt8M_ZCYfUJxMPS1Qictmx8VWdNffjSmVfKqr5Vu9DAwTkAzDoJuS0-5LBAdpaaIvhaDrOYjVpC-XGCU6jXscWea56qk0eFMm45l72NHT3nJWi-ZECrjgkA2RA1sq_bud0k78cWODtSmooCGbWJ5KgmVP9JYI2fsB2e3GTDUp40ZSL7GErBPmo27d2T_55zt5v6f1ki8EYgFalYtBAYvoC_w-dTJ2YMEeHDk76X5VY2wyUKLmokfZPHIyISYL2pQ0-GxwWryARooZlLZrDJq45UwD_EYtbDj67Fvme-yji3NC_pjC4Y42b1AW5p44T0mY6AAnSuR7tMK-X-5najafKWAVN7xMZtpb1jqkrfreXsta-AznSPQFX1gm5DiMvRsO47doxmByUyVXARJq30Zk2RBXg871pQMrCzYGD7BKo70o1NslfRLGzly4IvgrB5vyGzqcuOq4EDcGu2l543LI0-pPPFBvdWv_vLPODrPG-evtHlOIfEqbsl3qD_ysG8fwmvCOraRr4xk5c-p6SHBSpmrqw6lGhK5eBhd6dSmC3ToNcGGtk=","summary":[{"type":"summary_text","text":"**Planning controller line identification**"}]},{"id":"fc_0698befac979932d016a58ac02f5ac819ab01cf153f2043cc8","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating controller label assignment\",\"pattern\":\"sub.describe|writer-start|role=controller\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"650-820,1200-1650\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_40YUs4YmkN7JmtgBm0WGg5YA","name":"grep"}]},"duration":9093.396000000648,"ttft":4245.6214000005275,"contextSnapshot":{"promptTokens":206654,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_40YUs4YmkN7JmtgBm0WGg5YA|fc_0698befac979932d016a58ac02f5ac819ab01cf153f2043cc8","toolName":"grep","startedAt":"2026-07-16T10:01:41.225Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Locating controller label assignment"},"id":"cce90fad","parentId":"67db9973","timestamp":"2026-07-16T10:01:41.225Z"}
{"type":"message","id":"3c7709f7","parentId":"cce90fad","timestamp":"2026-07-16T10:01:41.237Z","message":{"role":"toolResult","toolCallId":"call_40YUs4YmkN7JmtgBm0WGg5YA|fc_0698befac979932d016a58ac02f5ac819ab01cf153f2043cc8","toolName":"grep","content":[{"type":"text","text":" 1557|                    \"writer-exit\",\n*1558|                    &format!(\"role=controller session={sid} reason=superseded\"),\n 1559|                );\n 1560|                return;\n 1561|            }\n...\n 1565|                    \"writer-exit\",\n*1566|                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1567|                );\n 1568|                return;\n 1569|            }\n...\n 1596|                    \"writer-exit\",\n*1597|                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1598|                );\n 1599|                return;\n 1600|            }\n...\n 1614|        \"writer-exit\",\n*1615|        &format!(\"role=controller session={sid} reason=channel-closed\"),\n 1616|    );\n 1617|}\n 1618|"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs","searchPath":"C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":4,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":4}],"truncated":false,"displayContent":" 1557│                    \"writer-exit\",\n*1558│                    &format!(\"role=controller session={sid} reason=superseded\"),\n 1559│                );\n 1560│                return;\n 1561│            }\n     │...\n 1565│                    \"writer-exit\",\n*1566│                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1567│                );\n 1568│                return;\n 1569│            }\n     │...\n 1596│                    \"writer-exit\",\n*1597│                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 1598│                );\n 1599│                return;\n 1600│            }\n     │...\n 1614│        \"writer-exit\",\n*1615│        &format!(\"role=controller session={sid} reason=channel-closed\"),\n 1616│    );\n 1617│}\n 1618│"},"isError":false,"timestamp":1784196101237}}
{"type":"message","id":"4923c173","parentId":"3c7709f7","timestamp":"2026-07-16T10:02:03.023Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing broker and daemon update behavior**\n\n**Summarizing broker and attach test behaviors**"},{"type":"toolCall","id":"call_9xuvSN91wPIt3Exv4s4UyZlD|fc_0698befac979932d016a58ac15d71c819a98046e72cec515e7","name":"irc","arguments":{"i":"Sending gap semantics detail","op":"send","to":"Main","message":"Extra proof: Brain::net_stream_subscribe inserts cursor=0 (brain.rs 1533-1541); first retained StreamLog frame after opener eviction has seq>0, and read_event rejects it as `stream N gap: got seq X want 0` (brain.rs 807-830). peek_first_line returns Err→DispatchOutcome::Failed; dispatch logs once then never clears claimed. This is stronger than misclassification. Existing attach restart test 772+ manually life2 serve_attach from 0 while opener still retained; daemon_refresh_e2e has no live rc stream. Broker physical role labels all IPC clients `role=brain`, so `role=brain stream-subscriber` in logs likely the per-attach dispatcher worker, not main heartbeat 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":2676,"output":573,"cacheRead":204288,"cacheWrite":0,"totalTokens":207537,"reasoningTokens":363,"cost":{"input":0.013380000000000001,"output":0.01719,"cacheRead":0.102144,"cacheWrite":0,"total":0.132714}},"stopReason":"toolUse","timestamp":1784196101291,"responseId":"resp_0698befac979932d016a58ac05f3f4819aa89d0bbbcd63b755","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ac097d40819aa2e1a8787fb02911","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwV22LlwPP4cu5NNvN6-YzsYmuGOlAF7YImyZLOcOkBQzUEAfge_N72u4a14sTXYkqeHBdOXjW0foUFqNvXszwuJoT-GNOevPp1VH1iune__gNt75yYyIB34tTG125Khz_z-C1PnG41lUh5txlBzlSV3pQmK-mp5CaQUdsRLyIJHQ34QBnjZYszE1GzMtm1MYcezlJXTcbT4j1z8xFmlw3xhbcE7kkaDsBsiUlLpa_GcIL4nuK_e3fXQ5tjpc9Cm1p2OroeW20y-WeZa7-qs--LeZQObK7bNQzxvJStPAYf62jEWOPSpO_0h2Qwvcn6r2GZ96M2XQWXRu6rzd1NJTIY0MVYUb7XKW2KOYK1PkgnssfY6clyR4tN_QjOtzt8I1ob5fVXI7mMohhRzPUMd6ESyF8XjEajSRV39JxaGDZ6bDeM4jdpXOWhNqUbuiI6fAsmsAA01lIzvm6guvvvRtSHkBXpsMCNFtU2TPTkwJwWVDN45sB6lI7xMhnRz7w6eFRar_apJV13ye9kVPK2j9KyO_quM_UyBRh1y7vYxTymYaRFhdi_M_6nyWk5UPpABwx_Nnyq9vpnkSeg_zVyxANXQhXVX9EbOAMKZzYi34Z5l1cxZ3a1TftFb6oaBhwTIt_zOC3rsKTZ3KCqbzXBpHjmapENd8XJ3hQaX0pp2awWObfLZEdThvbEwXwdOfZE8xp9785oJlwApv5i9WdjNMtkptPk4rJtn625WkbKsl1qT0s0pVapF4M7OKi-Btw2D4nD5h77FFdvBKTvsR4eWBM8qclpRzvMdA4ds5N6SY2H2dZrd5fGEKU2QMj0XKMJAD2ttIgYgwBL_AHTBqLNPoetAufGc0QCWEtA5V8btQ7C_pDqT79HO7ch7W2DRZUcUjbr0a7It9cheJuXRJAxR1CcGOZY8MEM-0RgMdhbrsMlrdbtlAw2D7m4p34lfU-J9avIuPqzyzpMT33nvlS3E481Ywi_BesWEqAWLt_ymOOC441wVNOTJHMtmTxzFRd1ONDk1YG1HJWRWU8yAE035ZjTjsX3hCDtaeIQDBdc79QcscH-4k9WUmsIH_IN0JNlcGrXFDR0-3nrAsyylQzQtXpA9FC7atg8MEFycMyyq2E46Otag9a2iC4O8TXxGB4iuhtMZMIi1UUZksSCzpPVB3Y-Sm8gaCoBt4PuJ3mdHzEZIxm5wjb1fJuaN2RlU9vUY4BZmH6G9vWdAnNMEy6S9HiOznFGmtZboPRzBJOuez-ghwyaJGccNWP6Z8JGiNuIPzOKmWQNcP6hwGNVZiOjpxn1aCUIbEb3ZBZd6Dn7yXKnNdgee1pqFhNNQjohQBGz2g9NrkA-igRsI61HOMLYZdvAFj9_SmT-1-gAIbchxShCJ84pIYlowIxatp32hdijTT9yjIN5AWaB11RiRwS-eE1rnoHDdWFHXmde0Kd66U77prQ963PfBvG1oxuTFFJ7UQ-j1G4YKdC-HDNTwRqc1GqUttmdw67j8ilgH9IFhQN0zUmcInMH-CDbR456XoQbxMOFJo0-4JCVvxjAc9OopZO8BkpIYIjnUDIg1p4bP_zkWiL2G5Q5bojLOXrD1prda3LLy9rjHXnHktgGxoReosDrFZXJX3OLqINdE88GDn2yQFMdLm6DyQ8tz8PQB9sG40OC6yeyrppzeXI6gHlE6kN2_VhGyWf28g562VDFUcCuau83u7B36C2L7PZRGzPc6OCqLWNcQwKYXWtD1CGixAhOwtANw0e6kqrX7T6j9barcsgHmTsp40JKXcdWNHG2ItQ38hdENk-4t6h_CcxK_7T_eKDJUyW3Z-unM0B78l_nZ3VgEIK7fYHhQI7d1DQAJvLiL1VqEDGj-8moMssxlworn92f-j0-GfkPi_HvYTiN10fu3xJR2hl-UKl9_Zt9TYTGodZjT411qijCNrnPFUsEZPGy_prC__kfjFcIIkTHq32uY1x_0bieR8G_dfPDCVa2LfB7-uA2ZL_Z74WB7hI2WmW1-rZaJtMgT2e6akMcnNvkWy3uIw-cENS36dT9Ui1llily4b4zf5wUJC8sO4Q2pwRCGYA6FyE_Z0iE7JllsHAgfbNsowr1nzgSIF4vd1FlBjejVCUKMt5DlZ0YRPA8T7BrDDd0jmt8616P1R30c2bK_L47myNSqjIvZ0DraNwlsXYdhlEsUyfed-zHR70pxtib7bPY74HFXWubmL5fJEPDYUVisfmVVwSYwBiy-rbm0iFaQQJrfaSXjcqlYeX96ik4RL2jNEP2dsVHV3o7Hc_nbsx3htQBsuBIrf-v9y03sKmafnfKdYieUNhR7IlrJ1cK_MFfOzXPo5AgmXr7CaacKM2cFj6BAybQjrEFeG_WmxzZ40E0mZrxCyHmF56u4srov0bwUOqmQWskyi3hO0VbA8t81sSafEXq2DMpDwPvJG5i3IeweI_QTENsO2e2egx2tXeojJup7NEL515UYxEXjDMl_N1NnyozPxS-5WqPRrGlpPmNL8peM2ob4aCWAEDQp12DxXeWFgLQbmr_gWHs1iyjvLcC24cv2M9wi4-c5ANIsZEfBpNS0rxTcPLszE-HGl0mFq9orJvW8k2cCIvQZi9u9oDkyP0E4wAiZjRtPOptxqJpNKMcdwQ4lzR3_ch0spkqV48m--3b5s-0U9Jprl2hbSVme_kY7b7jHMIlNUALYO0ozP7lvQKyPYdshV_0g5jc07X-MIzEYaflNogKNaKjWCx30BzNL54hIdeI77Km0k1EVdmq1zzhUXrSyoEdXwJU8zLwjepIDGJDKRMUSSzh3OA4rWSZGXe53-nA9IBiSrfRGj01N_RF-mWtWd5ZzKElHRWQeJsldPzFAR42z-0cZH867sM_ifniLeolH9CuNUTxwvfFdWtJnqw8ytTnssW4uY5EItBjouk23yN6luQyf4op849Rt_OzwWMEVZzw2lVztNuoeCZqGt6t2kAoOhnW7QAzoNqeMaoNQAksrp7bG9B2xHBEkAnCtQr_3nYW3RGbE8JhKYBxs9y-knsDNnAc14YhkDFO7Md18BMwtMHjSanM1_15RksHE3QYkcrIMR6pkhXY5xhI3fGC8Nts36RQsXkVwMV-eE_PX6QjG4PdYHJcbEEKVbGmp2cGAMjRHLLnLfOwl1a3cP9VPKZpxZcNO8nbe4r7tpaJdnopgIG7QX_CeDP54K2LmmM31lq8TK-zJv97qUsN8GMYeeAzhQr6Fan3QcpdLVk-4YMv05JEqALET1-L5HUX6xKeMw-BgKnIZXOiJFLW27koZUYpoAM5Uj1lTNEw1kWekro=","summary":[{"type":"summary_text","text":"**Analyzing broker and daemon update behavior**"},{"type":"summary_text","text":"**Summarizing broker and attach test behaviors**"}]},{"id":"fc_0698befac979932d016a58ac15d71c819a98046e72cec515e7","type":"function_call","status":"completed","arguments":"{\"i\":\"Sending gap semantics detail\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Extra proof: Brain::net_stream_subscribe inserts cursor=0 (brain.rs 1533-1541); first retained StreamLog frame after opener eviction has seq>0, and read_event rejects it as `stream N gap: got seq X want 0` (brain.rs 807-830). peek_first_line returns Err→DispatchOutcome::Failed; dispatch logs once then never clears claimed. This is stronger than misclassification. Existing attach restart test 772+ manually life2 serve_attach from 0 while opener still retained; daemon_refresh_e2e has no live rc stream. Broker physical role labels all IPC clients `role=brain`, so `role=brain stream-subscriber` in logs likely the per-attach dispatcher worker, not main heartbeat brain.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_9xuvSN91wPIt3Exv4s4UyZlD","name":"irc"}]},"duration":21717.175499999896,"ttft":4188.69280000031,"contextSnapshot":{"promptTokens":206964,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9xuvSN91wPIt3Exv4s4UyZlD|fc_0698befac979932d016a58ac15d71c819a98046e72cec515e7","toolName":"irc","startedAt":"2026-07-16T10:02:03.023Z","intent":"Sending gap semantics detail"},"id":"01fe208f","parentId":"4923c173","timestamp":"2026-07-16T10:02:03.023Z"}
{"type":"message","id":"d37ab6e2","parentId":"01fe208f","timestamp":"2026-07-16T10:02:03.024Z","message":{"role":"toolResult","toolCallId":"call_9xuvSN91wPIt3Exv4s4UyZlD|fc_0698befac979932d016a58ac15d71c819a98046e72cec515e7","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"RefreshLifecycle","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784196123023}}
{"type":"message","id":"552a0421","parentId":"d37ab6e2","timestamp":"2026-07-16T10:04:28.668Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Mapping file anchors and coverage**\n\n**Analyzing stream retention and handle inheritance issues**\n\n**Identifying dispatcher start dependency on network status**"},{"type":"thinking","thinking":"**Clarifying refresh trial behavior**"},{"type":"toolCall","id":"call_9IhPUC3Szah6xYn6LwINEP6A|fc_0698befac979932d016a58ac302af0819a9eab9b224b167b97","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Diagnosis: the parent hypothesis is directionally correct about which process death interrupts controllers, but incomplete about intended restoration. `spt daemon refresh` and normal in-place `spt update` kill only the supervised brain process. That kill destroys every brain-owned inbound-dispatch worker and its target-side `serve_attach` pump, while the broker retains the PTY child, PTY master, NetHost connection, both stream rows, stream rings, and the operator-side `spt rc` broker connection. However, the successor brain is intended to restore active controllers separately from `run_brain`'s cursor-only PTY-session resume: after its first 500 ms heartbeat, `spawn_net_consumers` starts a fresh dispatcher, which enumerates broker-held peer-initiated streams and re-runs `serve_attach`. Therefore `BRAIN_RESUMED` only mentioning session cursors is not by itself proof that rc streams are abandoned.\n\nThe strongest code-proven causal candidate is the dispatcher's non-durable, one-shot reconstruction protocol. `dispatch::run_dispatch_loop` creates a fresh per-brain `claimed: HashSet<u64>` and inserts each stream ID before spawning its worker (`dispatch.rs:205-225`). A worker reconstructs the protocol family by subscribing from sequence 0 and replaying the first NDJSON record (`dispatch.rs:245-301`). Attach stream openers are not durable metadata: the original `AttachRecord::Request` is merely sequence 0 in an ordinary bounded-overwrite `StreamLog`. `DEFAULT_STREAM_RING_CHUNKS` is 4096 (`nethost.rs:108-111`); `StreamLog::append` evicts the oldest entries forever once the ring exceeds the cap (`nethost.rs:206-225`). Ordinary attach streams are not made retentive. After the opener rolls out, `peek_first_line` calls `Brain::net_stream_subscribe(stream_id, 0)`, which explicitly seeds cursor 0 (`brain.rs:1527-1542`); the first retained frame has sequence >0, so `Brain::read_event` rejects it as `stream … gap: got seq … want 0` (`brain.rs:807-830`). The worker returns `DispatchOutcome::Failed`; the dispatcher logs `DISPATCH:<id>:Failed(…)` but never removes the ID from `claimed`, so it will never retry during that brain generation. Detach plus `spt rc` opens a fresh stream with a retained Request and a new, unclaimed stream ID, exactly matching the observed repair.\n\n[INFERENCE] The 4-of-5 partial observation is naturally explained if four older/busier controller request-side streams had advanced beyond their retained opener while one newer/quieter stream still retained sequence 0. The cap is measured in transport read chunks, not logical input records, and QUIC/loopback may coalesce or split writes, so the exact age/input threshold varies per stream. A second compatible explanation is independent worker failure: because each stream gets its own IPC connections but every claim is one-shot, transient replay/connect/subscribe failure can strand any subset while another succeeds. Logs can discriminate: look for `DISPATCH:<stream>:Failed(\"stream <stream> gap: got seq … want 0\")`; other `DISPATCH:*:Failed`, `CONN_WRITE_RETIRED`, or `CONN_WRITE_POISONED` records would support the transient-worker arm.\n\nRanked falsifiable candidates:\n1. HIGH — opener evicted from the bounded broker stream ring, producing a replay gap on successor-dispatcher classification, followed by permanent one-shot claim. This is fully established as a reachable source-level failure; field causality remains [INFERENCE] until the generation logs show the gap or the deterministic rig reproduces it.\n2. MEDIUM-HIGH — any transient successor worker failure is made permanent by inserting into `claimed` before work and never clearing/retrying Failed/Unknown. Falsify by showing all affected stream workers reached `serve_attach` and emitted successful stream/controller subscription lifecycle events after the new generation.\n3. MEDIUM — the refresh acknowledgment/readiness ordering creates a real pause and increases replay races: the CLI returns after the broker acknowledges only that the restart signal was accepted; the supervisor consumes it on a 200 ms tick, kills the old child, and the successor writes `brain.ready` before net consumers are spawned. Dispatcher startup waits for the first 500 ms heartbeat/net-status. This explains a short interruption, not a persistent until-detach stall by itself.\n4. LOW — bounded broker-connection write retirement during kill/replay can independently fail some workers. Current `BrokerConn` bounds and poisons physical IPC writes, so this should self-clean rather than globally wedge, but because dispatcher claims are one-shot even an ordinary retired worker connection can become persistent at the stream level.\n5. LOW / no direct evidence — inherited client pipe handles could delay old brain-connection EOF cleanup if a descendant inherited one. The source contains no transfer of ConPTY/forkpty handles into the brain: those remain broker-owned. `spawn_brain_child` is a normal managed child with `CREATE_NO_WINDOW` on Windows, not a detached broker/PTY owner. The designated long-lived detached daemon/shell paths use `bInheritHandles=FALSE`; ordinary short role spawns are not the resource boundary here. Delayed EOF also does not block a new subscriber: both stream and controller cleanup use pointer-identity guards, so late old-connection cleanup should not detach the newer subscriber. This candidate does not explain 4/5 as cleanly as the per-stream claim/replay fault.\n6. REFUTED for the reported refresh/default-update shape — service manager or broker lifecycle. `spt daemon refresh` never calls service start/stop. Normal bare `spt update` and `update fetch --apply` use brain-only `apply_staged`; the service manager is involved only in `update apply --finish` / `spt update --restart`, which deliberately stops and restarts the whole daemon. That full-cycle path kills broker PTYs and re-runs endpoints, unlike the reported live-PTY/stalled-view symptom.\n\nExisting coverage proves adjacent invariants but misses this contract. `daemon_refresh_e2e.rs` proves broker PID, harness PID, and ONLINE perch survival, but has no active `spt rc` stream/controller. `brain_survive.rs` proves PTY + QUIC resources survive a process swap, but not that a long-lived production-dispatched attach continues. `attach_survives_target_brain_restart_exactly_once` manually constructs life 2 and calls `serve_attach` from 0 while the opener is still retained; it bypasses production dispatcher claim/classification and ring-floor aging. Dispatcher tests cover normal production serving and surviving an unrelated Unknown stream, not restarting the dispatcher over an existing stream or retrying a failed claim. `resume_no_control_steal_e2e` and `brain_resume_conn_deadlock` test direct PTY subscribers/session resume, not the broker-held network stream and target-side attach worker. `rc` reconnect tests exercise broker/transport EOF; a brain-only restart leaves the broker and operator stream alive, so the frontend receives neither broker EOF nor stream EOF when the target serve worker vanishes and therefore does not enter reconnect.\n\nProposed safe deterministic feedback loop (not executed because this assignment is read-only): add a test-only NetHost constructor/ring-cap seam so the attach stream ring can be 2–4 chunks rather than relying on 4096 timing-sensitive OS reads. Start the production dispatcher and a real echo PTY under an isolated broker; establish an rc-equivalent loopback Control attach; prove output/input first. Advance the peer/request-side stream enough to evict the Request, then terminate dispatcher generation 1 and start generation 2 while keeping broker, PTY PID, operator connection, and stream IDs fixed. Require the SAME operator stream—without detach, redial, or a new stream ID—to accept new input and render its echo within a short deadline. Current code should fail with the stream-gap/one-shot-claim signature. Add two companion arms: (a) opener retained, which must already pass, preventing an overbroad restart fix; (b) injected fail-once worker/connect outcome, which must be retried and then pass, proving claim lifecycle rather than only opener persistence. The end-to-end production gate should then extend `daemon_refresh_e2e`: active controller before real CLI `daemon refresh`; brain PID/generation changes; broker and PTY PID stay fixed; same controller stream receives post-refresh echo; no detach/reattach. Capture brain/broker logs and assert a new-generation stream-subscribe plus controller silent re-take, not merely `brain.ready` or perch ONLINE.","files":[{"path":"crates/spt/src/cli.rs","description":"CLI dispatch and update shapes. `DaemonCmd::Refresh` routes at 1471-1477 to `cmd_daemon_refresh` at 4064-4090. Update dispatch is 1443-1456. `cmd_update_apply` at 4890-4955 chooses live `apply_staged` vs daemonless apply. `plan_update_legs`/`cmd_update_composite` at 4958-5044 make normal bare update FetchApply and reserve whole-daemon Finish for `--restart`. `cmd_update_apply_finish` at 5140-5229 explicitly stop/wait/start cycles the broker. `cmd_update_fetch` at 6110-6333 routes `--apply` to `cmd_update_apply(false)`."},{"path":"crates/spt-daemon/src/applyhost.rs","description":"Shared refresh/update handoff. `apply_staged` at 299-348 connects before swap, swaps/records pending, then calls `Brain::request_brain_restart`. `refresh_brain` at 354-368 performs the same restart request without swap/staging. The returned boolean is only the broker's honored acknowledgment, not restart completion."},{"path":"crates/spt-daemon/src/brain.rs","description":"`Brain::request_brain_restart` at 1459-1467 sends `KIND_BRAIN_RESTART` and waits only for `BrainRestarted{honored}`. `resume_sessions` vs `resume_session_cursors` at 1028-1090 distinguishes subscribing/draining callers from the supervised non-draining brain. `net_stream_subscribe` at 1527-1542 seeds the requested cursor. Stream data at 807-830 rejects a first retained sequence above that cursor as a gap."},{"path":"crates/spt-daemon/src/broker.rs","description":"Broker owns PTY sessions and client-role cleanup. `handle_conn` at 2970-3220 labels every IPC client generically `role=brain`, serves commands, and on connection end detaches only that connection's PTY/stream roles while leaving sessions/rings alive. Thus `role=brain stream-subscriber` logs can be per-attach dispatcher workers, not the main supervised heartbeat brain. `dispatch_brain_restart` at 3877-3898 raises the supervisor atomic and acks. `dispatch_subscribe` at 3449-3482 resolves a target serve worker to controller/viewer. `OutputLog::resolve_subscribe` at 1147-1210 silently re-takes controller for the same remote identity, enabling intended restart re-serve. `any_local_controller_wedged` at 2910-2940 is promotion gating only, not active rc stream reconstruction."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Exact process transition. `BrainRestart` at 106-140 is an atomic coalescing signal. `supervise_brain` consumes it at 887-891, hard-kills/waits the current brain, increments generation, and respawns with `StartReason::Update` at 913-922. `run_brain` at 155-315 cursor-resumes PTY session metadata, writes ready, starts brain-owned hosts, then on the first 500 ms heartbeat starts net consumers. `spawn_net_consumers` at 321-335 starts the dispatcher. `spawn_brain_child` at 1018-1069 is a managed plain child (`CREATE_NO_WINDOW` on Windows; own process group on Unix), not owner of broker PTY handles. `spawn_brain_supervisor` at 1095-1131 wires the production broker truth/reaper."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"Primary defect locus. `run_dispatch_loop` at 198-232 creates an in-memory per-generation `claimed` set, inserts before spawning, and never consumes worker completion to retry/remove Failed/Unknown. `peek_first_line` at 245-279 always subscribes from 0. `worker` at 283-302 classifies that replayed opener and returns permanent Failed/Unknown on replay/connect errors. Attach arm at 333-394 resolves endpoint/session and calls `serve_attach`. `spawn_dispatcher` at 690-697 logs only loop-level termination; per-stream failures are `DISPATCH:<id>:…`."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-held network data plane and opener-loss mechanism. `DEFAULT_STREAM_RING_CHUNKS=4096` at 108-111. `StreamLog` at 150-291 stores the opener only as ordinary ring data; `append` cap-evicts oldest frames at 206-225; `attach` replays only retained frames >= requested cursor at 256-281. `register_stream` at 640-700 keeps read pumps and rings broker-side across brain death. `open_stream` at 1260-1370 creates paired loopback stream rows for local rc. `stream_infos` at 1420-1450 is what successor dispatch enumerates. `subscribe_stream`/`detach_stream_sub` at 1500-1533 use subscriber replacement and pointer-guarded late cleanup."},{"path":"crates/spt-daemon/src/attach.rs","description":"Target-side control pump owned by dispatcher worker inside the brain. `serve_attach` at 235-540 subscribes the peer stream, creates a separate forwarding broker connection, replays `AttachRecord::Request`, and at 333-416 attaches to the PTY as remote Control/Viewer. When the brain is killed this worker dies without finishing the broker-owned network stream. A successful successor worker can silently re-take the same identity and continue; without it, PTY and operator stream remain alive but no process forwards between them."},{"path":"crates/spt/src/rc.rs","description":"Operator-side topology and symptom. Module docs at 1-24 establish rc as a broker-net-stream pump; `establish_attach` around 1330-1510 opens/subscribes the operator stream. `attach_viewport` at 1519-1628 reconnects only after `BrokerGone` or `Severed`. `pump` at 1820-2000 retries read timeouts forever after any prior event; it enters reconnect on broker IPC EOF or target stream EOF. Brain-only refresh preserves both broker and stream, while the dead target worker sends no EOF, so an established rc can remain silently blank until operator detach/new attach."},{"path":"crates/spt-daemon/src/conn.rs","description":"Physical broker IPC write boundary. `BrokerConn` provides bounded/cancelable/poison-on-failure writes, with lifecycle attribution. This prevents one dead consumer from globally pinning broker writes, but a per-stream worker connection retirement can still feed the dispatch one-shot-claim defect. Relevant log discriminators are `CONN_WRITE_POISONED`, `CONN_WRITE_RETIRED`, and `CONN_LIFECYCLE`."},{"path":"crates/spt-daemon/src/daemon.rs","description":"Broker lifecycle and handle boundary. `Daemon::run` binds/serves Broker/NetHost and starts the brain supervisor at 165-367; graceful whole-daemon stop raises brain_stop and reaps the brain subtree. `start_daemon`/`stop_daemon` at 554-596 invoke service routing only for explicit whole-daemon lifecycle. `spawn_detached` at 610-720 and Windows `detached_no_inherit` at 738-858 concern initial long-lived broker startup and captured-pipe safety, not routine brain refresh."},{"path":"crates/spt-daemon/src/service.rs","description":"Service-manager boundary. `plan_start`/`plan_stop` at 57-78; Linux registered default-home daemon uses `systemctl --user start/stop` at 149-180. Windows logon task is boot-only and `detected=false` at 245-265. Refresh and normal brain-only update do not traverse this module; finish/restart does."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Existing real CLI refresh gate. It verifies brain PID/generation cycle, unchanged broker PID, live harness PID, and ONLINE perch, but never opens an active rc/controller stream or checks same-stream post-refresh input/output. This is the central missing acceptance seam."},{"path":"crates/spt-daemon/tests/attach.rs","description":"`attach_survives_target_brain_restart_exactly_once` at 772-918 manually drops life 1 and explicitly calls `serve_attach` for life 2 from sequence 0. It validates retained-ring replay and journal dedup but bypasses production dispatcher classification/claim and does not age the Request beyond the ring floor."},{"path":"crates/spt-daemon/tests/dispatch.rs","description":"Production dispatcher tests cover normal families and `dispatcher_survives_an_unknown_stream` at 1092-1155, which proves one bad stream does not kill processing of a later stream. They do not require retry of the bad stream, restart a dispatcher over existing attach streams, or test opener eviction."},{"path":"crates/spt/tests/brain_survive.rs","description":"Process-level PTY + QUIC survival across swapped brain binaries. It proves the broker resource plane survives but not active controller forwarding through a successor production dispatcher."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Proves brain respawn does not seize direct PTY controller connections and promotion still succeeds. It does not exercise broker-held rc network streams or dispatcher reclassification."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Guards the former undrained session-subscription self-deadlock and justifies cursor-only main-brain resume. It is orthogonal to per-stream dispatcher workers, which intentionally subscribe and drain."},{"path":"docs/KNOWN-HAZARDS.md","description":"Domain invariants: §6.7 broker/brain process isolation, §7.36 subscriber/control decoupling, and §7.38 bounded physical broker writes. These support keeping PTY and network resource ownership broker-side while treating brain-owned view pumps as reconstructible control-plane state."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Architecture ruling: broker process owns PTY masters, children, sockets, NetHost/QUIC and cursor truth; brain is freely restartable and must reconstruct logic. It explicitly requires process-level survival but leaves active protocol-worker reconstruction dependent on broker stream replay."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Operational update rules: normal signed apply, brain-only vs broker-touching classification, and refusal of broker-touching updates while broker-held runtime is live. Confirms default refresh/apply should not cycle broker resources."}],"architecture":"Four distinct layers must not be conflated:\n\n1. PTY data plane / resource ownership. `spt daemon run` is the long-lived broker process. `Broker` owns the PTY master/ConPTY/forkpty session, child process, `OutputLog`, input journal, NetHost, live connection table, and per-stream `StreamLog`. None of those OS PTY handles are handed to the brain. This is why terminal children and rings remain alive during refresh.\n\n2. Operator/viewer connection. The `spt rc` process has its own local broker IPC `Brain` connection. It opens an operator-side network stream row and subscribes to output records. Its physical broker IPC connection and broker-held stream survive target brain refresh. It only reconnects on broker IPC EOF or network stream EOF; target pump death alone produces neither.\n\n3. Target-side controller/viewer bridge. The brain-owned inbound dispatcher enumerates peer-initiated stream rows. Each row gets a worker with a fresh broker IPC connection. `serve_attach` subscribes that peer stream, interprets the replayed Request, and subscribes the PTY OutputLog as Control/Viewer; a second connection forwards PTY output onto the operator stream. This worker is the actual controller bridge. Its physical broker connection is logged as `role=brain`, then gains `stream-subscriber` and controller/session attribution; `role=brain` is the protocol handshake class for all IPC clients, not proof that it is the singleton heartbeat brain.\n\n4. Daemon brain and broker lifecycle. Refresh/in-place apply sends `KIND_BRAIN_RESTART` over a transient CLI broker connection. The broker acks acceptance, its supervisor hard-kills the whole brain process, and respawns it. All dispatcher workers die with that process. Broker connection cleanup removes their logical subscriber/controller roles but not PTYs or streams. The new brain's `resume_session_cursors` restores only main-brain cursor knowledge without subscribing. Independently, first-heartbeat `spawn_net_consumers` starts a new dispatcher that is supposed to reconstruct active stream workers from broker truth.\n\nThe reconstruction boundary is not currently durable enough. Broker stream truth records direction, IDs, connection, origin, sequence/ring, and finished state, but not protocol family or attach opener metadata. The successor therefore depends on sequence-0 payload replay. Because that payload can be cap-evicted and claim failure is permanent for the generation, a resource-surviving refresh can leave an operator stream and PTY alive with no target bridge—precisely the detach/reattach-heals shape. The safe architectural direction is to make protocol/attach claim identity broker-durable (or otherwise recoverable without ancient payload replay) and make dispatcher claims transactional/retryable: in-flight only while a worker is starting, committed while a worker is serving, released/retried on recoverable failure. Readiness should also not imply controller continuity until restartable protocol workers have been reconciled, or the CLI must explicitly say it acknowledged rather than completed the refresh."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3271,"output":5343,"cacheRead":204288,"cacheWrite":0,"totalTokens":212902,"reasoningTokens":590,"cost":{"input":0.016355,"output":0.16029000000000002,"cacheRead":0.102144,"cacheWrite":0,"total":0.278789}},"stopReason":"toolUse","timestamp":1784196123046,"responseId":"resp_0698befac979932d016a58ac1b6890819aac743596dbebcc2a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0698befac979932d016a58ac1f169c819aa47b68eea8e650cc","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwul-QynLMFI9DdfejGWXmSjjzskcx7QQZoXZ_Td-E0ryVWquVr2T101QssjhPfZNRz-po8eSUqQHZVa1arcgienC1EUT0dNW9XM8u-gKMBHNw7O8XBxd5GE_1QtjSjdVk-zzHO3EZGnTboltqDTqAy-2lV6Yta2zctrSPKuCQtnLClM4J7ikDi_WHMyRWXn14xm0U4w26vZ2raWtPQ7TStmSZtWUk9NRB7pHw4bGxP22LKEsgaw5PCDpupX0lxRHmleLu1zEcgbnFnlBj25rVX84G5Hm-3Uy47fYtlt3Ahy-0DOIiGYvncMhU5RKAu05gZX_a_qcQdyqR60YnUCYMbOHUaszW6FZHqXF-WI5gszerfbNI3NUicZctm61My-6YsxjDqmqNsXCFJuX_kzE7R8x8vgjRwUAYM48gC9aP7MCoRkJq1AgjugivTKeaFtYCt9dyWBVoRyEA2kjjI4xJvS7mWk0i3N24oQt98fE1Ess7my3i2u2HJqFn6T8cau0eQtx5D0W87HaIDh0JW1zRCBYArnRnzQ474BTDUyQEyOUrM2pFjWK6MZzHbgKUYMgL3gI6FMWEAWw-FbW_vbKqMfFzq07By6KR86o12oxz0jzwI4jVSjjEMZ4rPkxfjoyZNrAyJnEnzUr1fXtKjfxDC7nbMVybkmmKZgVk6s3JXgNLvlj1XcGmO-jwaD4IB2IQDLhUK3q9pHZZlsrdfFJ_FJEFZ_pUntL5HKauvrtmWoQrAo3ofsaP9nhHHeTFcoQei6JKtoFO8EbpKHAhzyedk71hk-mc6RrC29EEyXq2gbqVphl2B-AJ1s0Hsm720MJAONVy3yf5bT4w4Fnwnn-BAyz-vtwS3E5k4E8O_smHp0IeJVOzmaaovrPNncy0BQwSG4mPgoE1tle6FhAClfnt4oDCpArpAHS_63U3EbQT1Fg_yZi4yq228rx4V57p1LkwOqm4KU5diqHyRUpY67zHIr1g4d9H-fVkGEep2NlmAYJoZscGgDpD6QwoKPxPbF7cN7BykzWm9aQMVahrsf4u4PzNIstgMvMfvpdqKFn6N3pT3O2xoiJPy-n0szryACc5QZkfqXziCvsG5l0nO9Tx5x2PLzM163IN6DQIdOwX-5o58q30J-i6Pak_9EejKaMp-hogayomtjaTZ3xqGDT2t0V2cBCWNpQzWs7yw9sO6Abau8c5M2WCQ0Ux6ydUhc6D7eGxD_bP8v5An44LYJk-OJzOcI6nZ0MBQs1zraVpg3syXNsnfU_itigoRS0Tg7mzluPXZsjSnzPzhEgY-0Dh517kvC3hta_iTwKGtWv10Sm5l9ifzs_8UgZ0_f0h6BTmsyv3XjJPKlC06t2aJu9Pe27BVS4meIgrbZYJ8VLzO8noM7WntoL7KqaILMxAkus3e5TmC34upXPW9MXzLo-gcbS3A-at89w0Y7VgEpNKX-WQZkgLKVP2-Pwr82M6CLrszVEJ9Zotc98qwQUo-sjPE3kyYU8XFyaZQKzMhiPUI0HE--eoLXm937HYvZi7ew_Twm2JtZJS2AuDPE3CLRXGyQdJYSZcRDfoR_pCvQCytfyPKswS209kGtq_ORn9-UMizw0OHIX6yb5NTkJ5VtCbP0hqh1x4Bx38B3fMrkiGOn_4RWIm1op4KqzqVSCnSqv0F8GPeIHKGL3dDBKWW9iYz5otoBIyRSVVjxTxRzd1_iitDdfGpnG9eKGXcXliP3Q6574_M570bqFKyjAy8WQcg9y71scRxfN7g7zmCR0cjq-Z9YoYVYK9X5SO_2aVznJyNjpaHrbEc5nYxdaMRltlfZk7w5EAfeorHuCNV2uSuvGvOna-wCFDm7vCK58CUFab9bRbOYRKTpeXCbR9WMGANdPn0kwW5GnACBJDkaR5ogvUpEePAPnATqRX2hFuCJIz03Wpd2LSCyVu0L3IZbzbKPgGYkB9B8OCruqelAPmu2busCP92TINL8bIRYMW22x-SyRDaLkELs0trQ_iCS5mR7bjPxLHXhq-fcg-C7X_xtQte5ng2IIxTlq28Do15_8Hr0Ik7JylehMOcp14YWWZKeXdgu5iP_lOBDj1tuxw8_DfNsbcCQruUr0AXdLcllh6Nv3Ee7jQ35tqnTV7KkEBiDHitHHa3G1fnMktf8fTw_0DKpOAige21ioYWepJy1qrutMd7G8kudPwzg0WB_gc5kIDIocMIcmK6qBT4LV81qm7SP5IygzDUkRtEi7zStYOdQ-XSDQMS2hQh7GbYjtt2H0WBmogzbU_eeeNl-IiCH9p53GBNm-e0ZbuvX9Eal_pv2jWI5S3Oe9kga_uM55Ol4YkDDKpkubvxbNdYgN-WCT-wVo-CcGEI2JvypaYVj6Q5jgjxg8UgXfaHeGwPyJnmKnrll6IVLn1Z8cx6cEFqhOO1Jgtx5a_qfDZlG3L8JUwJdd52mgGU0UvQrC6aDyWXMrE6cQLYyuIJJYvrQ-BR-TaZM6t2DBHY4iK2-HCNxBsmUgEat4TF5oPs_spR-1ztm9NBTIiNCsqIqT4qp6J0kECSFce62ueGjWm-oBgMrqAjMLqhpPFI_hL1xdHa3BcRIp3LFkMTMiL7L4BnJTkA7TOOjjFdulhzgGg7TCS6RZFki6RRwIOxVwfcy4YwhMCU818lm-wSslBcWIdPfpNCmFJtFwRHTp-fLltc_FoPr0q_l8EEOLI2twiUxGAGxTJl0TTOgkGwr5O21u3WFVyPWXyf5KNGqBaaiKzoZz7MUdDJ_R7G-1V39UUmlc8KKQ58vRGOXqRsRD7-nN-qf_jo2xgez5SUqEeNNS81NkTjaa2AHcoCqOEcQl1F8H3jc-c0L1HUOIWR9eh0dk1UwY5mnEPD765CWizonuiC9e5A4eWGxnY76qz-fkC6MTV7r3xzRw-d9naXkTm9AL0DNqLAYtUhjYYPMC_f0GfpIbt0m1xNKIRokD1I8CGr8QvieLDQ15YHEPRtJlE42sSeMYPereL3bPsL7t5E8kt_-mEcy-Q9qxf5mlkpGrW8TR8LZjWFncHY9yXy_wDF7iKWE_XKY-FsBDTK6oRw5IM0e6UElzmmXAmfGCqQ1k7RiV0a891swiMhWk0U-zP08vMBphuAztdfLA93rfTEzSmv0sGmCMqHvPGf3YUJJF59uJ9pHtvgWBf7euwbPMGdqxaxUfiw2svVOgxXfePszpbcK7T62aiyG2XjZ4xXwCHUogX5GuVOfNTiF1ZqTIPO6tUa9gEuGb5PETWCsMvmWwrWhYNrcw6QfX9KHs85q2rTIFcFXn5uYWcR4SMcJVQ5ad8zNnBzwyb_Hps1-MHm0jfPsIoewXbRGnVdcOqPbG199dpEYLMk-7ZpfI_9PiN-hfZaZXOgJyEuSaqQYQEMyj2fR2D3QDPlj8ioySL9t4tBQrEtI80itG1pGMxS5u0qfnVTkZ5sdZihEu6uUtJMsvXhV0UaqFnTR5B9FzSafuCdHQwntL6-QuyZDYKfjwojAl8z5NOcPcs8TRVKzXhbPfxmH9jIcmt08z6s0pcuJtzTaee0UndFJktkmuFA72IDcefnn7XJqGumq6diSaKGDZhPeQKxERmtesiFLmZfP6VoEwgFtalVOL3G8nIdaxEiONNN5bjhx1EIT27niD_uq8S8VJYBUlhYVQ__fQbfgl2EPpbv_T4HhUJw_f6VJ9oGmJ733uvG-Xbu1MUaVPbaDj0goFtwGuXwK-u6xUoA0oHrMdZz9RcbSgElQnOlBjRAhZNMvAm3uPMJwpqrSzvoAW2jU6A3LKm20FdTzj_JEOV6cY1dISW-2dl33qYGLR6Gg_DbFytMmnYB0fX2va4HO5JQ5DGaSf3NtS_NmT6D1jyd5nkApbB377GGMnSYydl7tkXtWsui-FoLP1kTyoy3ozt7Lot_kXIgFWldDgT-5eftkPskifoLMCmuY6epHzg7EOWwuF98pInHE82PBqIC7PNRRfT9w-qpluNzSkSgdROg15rO7R1wPC_yucDeoOoj6mCRMGihedmBHAYzLcWcvSQJgz6VMKnMkZPPtg9mZZxZGGLsmsXj0w==","summary":[{"type":"summary_text","text":"**Mapping file anchors and coverage**"},{"type":"summary_text","text":"**Analyzing stream retention and handle inheritance issues**"},{"type":"summary_text","text":"**Identifying dispatcher start dependency on network status**"}]},{"id":"rs_0698befac979932d016a58ac2e273c819a94ec05398b5e208a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwwKaZe_x80zdvnz45C20GPPdU_q9g5_fvNMoXwrmA_D1O_9xcpp0TWfOffRR8CFKezRisSrRg2VPCusee2FTdJgy4BK2wcW_rRi9vXay6hekty4806ToUbYg8n-ml4BKnYo2t2ERq3svAafRKcAqeaX2phMc5a81mJ5gFAVrBNW8NljXVQbkx8gaftNQbbEdC6ieq5bwPL30JDfoqPBNRq-B-m1p0dFkuy52XCV9BNfcesmeuYl2CSJCsz7HOPNXPWBsaEiCxOUUkVtshvnErWWH8s1FpNeNV4NF38NiKqUKJsI5s9GWwNr2qE0M1tvkkeX2SR1XGH4usiSFuk7Kh0SA4Xsj2MQ7ElZBthpPIcjpdPAXbnss1aW0B6v_p585NEvbC9wnWjBWND8Khm4-jYgCdwKB0rJL7Gr9nvwbCSBXhLJ-qmBfUuKTjV-KYdwtbjDgx5vyfehXn5foO676qUH9RP9YbyyHtIkN4K2orl7i_EdAyphPgkWVwPoOCaL88dNn9nnNuFLpCk4BWH9myzahfM96nkKNyQ3HXpDKoVCHOqihduBzGndqH6w8VvNiC-8QFBEFmdW3AR50knrzp-XFZKogvv4T2CSnuuWCVrSzYw5ms7d5na2N34QpOeWsEH6DLhg7J_yA7SUsG_iYZfElMh4WperMhVVTOFfPP10uj6mx0gt-jCJnQzvC4wMHWvWJgUF4h2Vb3g3OGnY4vwLqinCJSamzzSDZzbeoRvHlz4iB2twZVd6r2ThRpwBWg-BebZM7Nud7OTVIpPxVAETYvOy1s_Pk8BQMwA5TWEzD1ISCh9VTqLHMCjBklc4T4_m7A535hRn0wUSp0gfhY8gzqKEGNORp9_iPuLMZ_gp3iNoIHiDEuTz_5WUtCkU7gvwWwYlmP_HYylsMBqJO6s6y3yIN6WulOJSscflemeLrRNcGft-TJPeNyu8N8tAZuuY1IEjFtJTNloUf1-Nc17oNMbc1Gw8nqZfYodhxdHdJASJLnfiGDxMVDfXcD_9Zy-yQz-5SoxYxLdulVxHEHlCaZVIZSwY7bR0QgK47Yji6-Hbw5KEcFEAtMvzlc_yqxohnTxcVcYKiH8It4VW36AfDUV1LhcKprvKpnE880t0HppdeyHTbDgg6P5WL0squAKo_pn7xFyLrcQSQbsvrHe1Ub8gfsL5S2XyVOnvAVl814rI7TBDOvxG1qb6ylNrMpBRUpJKa2IlXdKDfp3PH7lHgLhcQhxNqB8WvHAYO2zIn4IOfp-wcr4mNuHKJcB-Zi2lj5rCUgi97PNoiNEzm9kyN2iQ59a7kF6fxRnuH_Eh6hVHsxeg_VHNpq5giBq4AEKz8mNsg6_qrgSV_ULPVPhzg==","summary":[{"type":"summary_text","text":"**Clarifying refresh trial behavior**"}]},{"id":"fc_0698befac979932d016a58ac302af0819a9eab9b224b167b97","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Diagnosis: the parent hypothesis is directionally correct about which process death interrupts controllers, but incomplete about intended restoration. `spt daemon refresh` and normal in-place `spt update` kill only the supervised brain process. That kill destroys every brain-owned inbound-dispatch worker and its target-side `serve_attach` pump, while the broker retains the PTY child, PTY master, NetHost connection, both stream rows, stream rings, and the operator-side `spt rc` broker connection. However, the successor brain is intended to restore active controllers separately from `run_brain`'s cursor-only PTY-session resume: after its first 500 ms heartbeat, `spawn_net_consumers` starts a fresh dispatcher, which enumerates broker-held peer-initiated streams and re-runs `serve_attach`. Therefore `BRAIN_RESUMED` only mentioning session cursors is not by itself proof that rc streams are abandoned.\\n\\nThe strongest code-proven causal candidate is the dispatcher's non-durable, one-shot reconstruction protocol. `dispatch::run_dispatch_loop` creates a fresh per-brain `claimed: HashSet<u64>` and inserts each stream ID before spawning its worker (`dispatch.rs:205-225`). A worker reconstructs the protocol family by subscribing from sequence 0 and replaying the first NDJSON record (`dispatch.rs:245-301`). Attach stream openers are not durable metadata: the original `AttachRecord::Request` is merely sequence 0 in an ordinary bounded-overwrite `StreamLog`. `DEFAULT_STREAM_RING_CHUNKS` is 4096 (`nethost.rs:108-111`); `StreamLog::append` evicts the oldest entries forever once the ring exceeds the cap (`nethost.rs:206-225`). Ordinary attach streams are not made retentive. After the opener rolls out, `peek_first_line` calls `Brain::net_stream_subscribe(stream_id, 0)`, which explicitly seeds cursor 0 (`brain.rs:1527-1542`); the first retained frame has sequence >0, so `Brain::read_event` rejects it as `stream … gap: got seq … want 0` (`brain.rs:807-830`). The worker returns `DispatchOutcome::Failed`; the dispatcher logs `DISPATCH:<id>:Failed(…)` but never removes the ID from `claimed`, so it will never retry during that brain generation. Detach plus `spt rc` opens a fresh stream with a retained Request and a new, unclaimed stream ID, exactly matching the observed repair.\\n\\n[INFERENCE] The 4-of-5 partial observation is naturally explained if four older/busier controller request-side streams had advanced beyond their retained opener while one newer/quieter stream still retained sequence 0. The cap is measured in transport read chunks, not logical input records, and QUIC/loopback may coalesce or split writes, so the exact age/input threshold varies per stream. A second compatible explanation is independent worker failure: because each stream gets its own IPC connections but every claim is one-shot, transient replay/connect/subscribe failure can strand any subset while another succeeds. Logs can discriminate: look for `DISPATCH:<stream>:Failed(\\\"stream <stream> gap: got seq … want 0\\\")`; other `DISPATCH:*:Failed`, `CONN_WRITE_RETIRED`, or `CONN_WRITE_POISONED` records would support the transient-worker arm.\\n\\nRanked falsifiable candidates:\\n1. HIGH — opener evicted from the bounded broker stream ring, producing a replay gap on successor-dispatcher classification, followed by permanent one-shot claim. This is fully established as a reachable source-level failure; field causality remains [INFERENCE] until the generation logs show the gap or the deterministic rig reproduces it.\\n2. MEDIUM-HIGH — any transient successor worker failure is made permanent by inserting into `claimed` before work and never clearing/retrying Failed/Unknown. Falsify by showing all affected stream workers reached `serve_attach` and emitted successful stream/controller subscription lifecycle events after the new generation.\\n3. MEDIUM — the refresh acknowledgment/readiness ordering creates a real pause and increases replay races: the CLI returns after the broker acknowledges only that the restart signal was accepted; the supervisor consumes it on a 200 ms tick, kills the old child, and the successor writes `brain.ready` before net consumers are spawned. Dispatcher startup waits for the first 500 ms heartbeat/net-status. This explains a short interruption, not a persistent until-detach stall by itself.\\n4. LOW — bounded broker-connection write retirement during kill/replay can independently fail some workers. Current `BrokerConn` bounds and poisons physical IPC writes, so this should self-clean rather than globally wedge, but because dispatcher claims are one-shot even an ordinary retired worker connection can become persistent at the stream level.\\n5. LOW / no direct evidence — inherited client pipe handles could delay old brain-connection EOF cleanup if a descendant inherited one. The source contains no transfer of ConPTY/forkpty handles into the brain: those remain broker-owned. `spawn_brain_child` is a normal managed child with `CREATE_NO_WINDOW` on Windows, not a detached broker/PTY owner. The designated long-lived detached daemon/shell paths use `bInheritHandles=FALSE`; ordinary short role spawns are not the resource boundary here. Delayed EOF also does not block a new subscriber: both stream and controller cleanup use pointer-identity guards, so late old-connection cleanup should not detach the newer subscriber. This candidate does not explain 4/5 as cleanly as the per-stream claim/replay fault.\\n6. REFUTED for the reported refresh/default-update shape — service manager or broker lifecycle. `spt daemon refresh` never calls service start/stop. Normal bare `spt update` and `update fetch --apply` use brain-only `apply_staged`; the service manager is involved only in `update apply --finish` / `spt update --restart`, which deliberately stops and restarts the whole daemon. That full-cycle path kills broker PTYs and re-runs endpoints, unlike the reported live-PTY/stalled-view symptom.\\n\\nExisting coverage proves adjacent invariants but misses this contract. `daemon_refresh_e2e.rs` proves broker PID, harness PID, and ONLINE perch survival, but has no active `spt rc` stream/controller. `brain_survive.rs` proves PTY + QUIC resources survive a process swap, but not that a long-lived production-dispatched attach continues. `attach_survives_target_brain_restart_exactly_once` manually constructs life 2 and calls `serve_attach` from 0 while the opener is still retained; it bypasses production dispatcher claim/classification and ring-floor aging. Dispatcher tests cover normal production serving and surviving an unrelated Unknown stream, not restarting the dispatcher over an existing stream or retrying a failed claim. `resume_no_control_steal_e2e` and `brain_resume_conn_deadlock` test direct PTY subscribers/session resume, not the broker-held network stream and target-side attach worker. `rc` reconnect tests exercise broker/transport EOF; a brain-only restart leaves the broker and operator stream alive, so the frontend receives neither broker EOF nor stream EOF when the target serve worker vanishes and therefore does not enter reconnect.\\n\\nProposed safe deterministic feedback loop (not executed because this assignment is read-only): add a test-only NetHost constructor/ring-cap seam so the attach stream ring can be 2–4 chunks rather than relying on 4096 timing-sensitive OS reads. Start the production dispatcher and a real echo PTY under an isolated broker; establish an rc-equivalent loopback Control attach; prove output/input first. Advance the peer/request-side stream enough to evict the Request, then terminate dispatcher generation 1 and start generation 2 while keeping broker, PTY PID, operator connection, and stream IDs fixed. Require the SAME operator stream—without detach, redial, or a new stream ID—to accept new input and render its echo within a short deadline. Current code should fail with the stream-gap/one-shot-claim signature. Add two companion arms: (a) opener retained, which must already pass, preventing an overbroad restart fix; (b) injected fail-once worker/connect outcome, which must be retried and then pass, proving claim lifecycle rather than only opener persistence. The end-to-end production gate should then extend `daemon_refresh_e2e`: active controller before real CLI `daemon refresh`; brain PID/generation changes; broker and PTY PID stay fixed; same controller stream receives post-refresh echo; no detach/reattach. Capture brain/broker logs and assert a new-generation stream-subscribe plus controller silent re-take, not merely `brain.ready` or perch ONLINE.\",\"files\":[{\"path\":\"crates/spt/src/cli.rs\",\"description\":\"CLI dispatch and update shapes. `DaemonCmd::Refresh` routes at 1471-1477 to `cmd_daemon_refresh` at 4064-4090. Update dispatch is 1443-1456. `cmd_update_apply` at 4890-4955 chooses live `apply_staged` vs daemonless apply. `plan_update_legs`/`cmd_update_composite` at 4958-5044 make normal bare update FetchApply and reserve whole-daemon Finish for `--restart`. `cmd_update_apply_finish` at 5140-5229 explicitly stop/wait/start cycles the broker. `cmd_update_fetch` at 6110-6333 routes `--apply` to `cmd_update_apply(false)`.\"},{\"path\":\"crates/spt-daemon/src/applyhost.rs\",\"description\":\"Shared refresh/update handoff. `apply_staged` at 299-348 connects before swap, swaps/records pending, then calls `Brain::request_brain_restart`. `refresh_brain` at 354-368 performs the same restart request without swap/staging. The returned boolean is only the broker's honored acknowledgment, not restart completion.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"`Brain::request_brain_restart` at 1459-1467 sends `KIND_BRAIN_RESTART` and waits only for `BrainRestarted{honored}`. `resume_sessions` vs `resume_session_cursors` at 1028-1090 distinguishes subscribing/draining callers from the supervised non-draining brain. `net_stream_subscribe` at 1527-1542 seeds the requested cursor. Stream data at 807-830 rejects a first retained sequence above that cursor as a gap.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"Broker owns PTY sessions and client-role cleanup. `handle_conn` at 2970-3220 labels every IPC client generically `role=brain`, serves commands, and on connection end detaches only that connection's PTY/stream roles while leaving sessions/rings alive. Thus `role=brain stream-subscriber` logs can be per-attach dispatcher workers, not the main supervised heartbeat brain. `dispatch_brain_restart` at 3877-3898 raises the supervisor atomic and acks. `dispatch_subscribe` at 3449-3482 resolves a target serve worker to controller/viewer. `OutputLog::resolve_subscribe` at 1147-1210 silently re-takes controller for the same remote identity, enabling intended restart re-serve. `any_local_controller_wedged` at 2910-2940 is promotion gating only, not active rc stream reconstruction.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"Exact process transition. `BrainRestart` at 106-140 is an atomic coalescing signal. `supervise_brain` consumes it at 887-891, hard-kills/waits the current brain, increments generation, and respawns with `StartReason::Update` at 913-922. `run_brain` at 155-315 cursor-resumes PTY session metadata, writes ready, starts brain-owned hosts, then on the first 500 ms heartbeat starts net consumers. `spawn_net_consumers` at 321-335 starts the dispatcher. `spawn_brain_child` at 1018-1069 is a managed plain child (`CREATE_NO_WINDOW` on Windows; own process group on Unix), not owner of broker PTY handles. `spawn_brain_supervisor` at 1095-1131 wires the production broker truth/reaper.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"Primary defect locus. `run_dispatch_loop` at 198-232 creates an in-memory per-generation `claimed` set, inserts before spawning, and never consumes worker completion to retry/remove Failed/Unknown. `peek_first_line` at 245-279 always subscribes from 0. `worker` at 283-302 classifies that replayed opener and returns permanent Failed/Unknown on replay/connect errors. Attach arm at 333-394 resolves endpoint/session and calls `serve_attach`. `spawn_dispatcher` at 690-697 logs only loop-level termination; per-stream failures are `DISPATCH:<id>:…`.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Broker-held network data plane and opener-loss mechanism. `DEFAULT_STREAM_RING_CHUNKS=4096` at 108-111. `StreamLog` at 150-291 stores the opener only as ordinary ring data; `append` cap-evicts oldest frames at 206-225; `attach` replays only retained frames >= requested cursor at 256-281. `register_stream` at 640-700 keeps read pumps and rings broker-side across brain death. `open_stream` at 1260-1370 creates paired loopback stream rows for local rc. `stream_infos` at 1420-1450 is what successor dispatch enumerates. `subscribe_stream`/`detach_stream_sub` at 1500-1533 use subscriber replacement and pointer-guarded late cleanup.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"Target-side control pump owned by dispatcher worker inside the brain. `serve_attach` at 235-540 subscribes the peer stream, creates a separate forwarding broker connection, replays `AttachRecord::Request`, and at 333-416 attaches to the PTY as remote Control/Viewer. When the brain is killed this worker dies without finishing the broker-owned network stream. A successful successor worker can silently re-take the same identity and continue; without it, PTY and operator stream remain alive but no process forwards between them.\"},{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"Operator-side topology and symptom. Module docs at 1-24 establish rc as a broker-net-stream pump; `establish_attach` around 1330-1510 opens/subscribes the operator stream. `attach_viewport` at 1519-1628 reconnects only after `BrokerGone` or `Severed`. `pump` at 1820-2000 retries read timeouts forever after any prior event; it enters reconnect on broker IPC EOF or target stream EOF. Brain-only refresh preserves both broker and stream, while the dead target worker sends no EOF, so an established rc can remain silently blank until operator detach/new attach.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"Physical broker IPC write boundary. `BrokerConn` provides bounded/cancelable/poison-on-failure writes, with lifecycle attribution. This prevents one dead consumer from globally pinning broker writes, but a per-stream worker connection retirement can still feed the dispatch one-shot-claim defect. Relevant log discriminators are `CONN_WRITE_POISONED`, `CONN_WRITE_RETIRED`, and `CONN_LIFECYCLE`.\"},{\"path\":\"crates/spt-daemon/src/daemon.rs\",\"description\":\"Broker lifecycle and handle boundary. `Daemon::run` binds/serves Broker/NetHost and starts the brain supervisor at 165-367; graceful whole-daemon stop raises brain_stop and reaps the brain subtree. `start_daemon`/`stop_daemon` at 554-596 invoke service routing only for explicit whole-daemon lifecycle. `spawn_detached` at 610-720 and Windows `detached_no_inherit` at 738-858 concern initial long-lived broker startup and captured-pipe safety, not routine brain refresh.\"},{\"path\":\"crates/spt-daemon/src/service.rs\",\"description\":\"Service-manager boundary. `plan_start`/`plan_stop` at 57-78; Linux registered default-home daemon uses `systemctl --user start/stop` at 149-180. Windows logon task is boot-only and `detected=false` at 245-265. Refresh and normal brain-only update do not traverse this module; finish/restart does.\"},{\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"description\":\"Existing real CLI refresh gate. It verifies brain PID/generation cycle, unchanged broker PID, live harness PID, and ONLINE perch, but never opens an active rc/controller stream or checks same-stream post-refresh input/output. This is the central missing acceptance seam.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"`attach_survives_target_brain_restart_exactly_once` at 772-918 manually drops life 1 and explicitly calls `serve_attach` for life 2 from sequence 0. It validates retained-ring replay and journal dedup but bypasses production dispatcher classification/claim and does not age the Request beyond the ring floor.\"},{\"path\":\"crates/spt-daemon/tests/dispatch.rs\",\"description\":\"Production dispatcher tests cover normal families and `dispatcher_survives_an_unknown_stream` at 1092-1155, which proves one bad stream does not kill processing of a later stream. They do not require retry of the bad stream, restart a dispatcher over existing attach streams, or test opener eviction.\"},{\"path\":\"crates/spt/tests/brain_survive.rs\",\"description\":\"Process-level PTY + QUIC survival across swapped brain binaries. It proves the broker resource plane survives but not active controller forwarding through a successor production dispatcher.\"},{\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"description\":\"Proves brain respawn does not seize direct PTY controller connections and promotion still succeeds. It does not exercise broker-held rc network streams or dispatcher reclassification.\"},{\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"description\":\"Guards the former undrained session-subscription self-deadlock and justifies cursor-only main-brain resume. It is orthogonal to per-stream dispatcher workers, which intentionally subscribe and drain.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Domain invariants: §6.7 broker/brain process isolation, §7.36 subscriber/control decoupling, and §7.38 bounded physical broker writes. These support keeping PTY and network resource ownership broker-side while treating brain-owned view pumps as reconstructible control-plane state.\"},{\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"description\":\"Architecture ruling: broker process owns PTY masters, children, sockets, NetHost/QUIC and cursor truth; brain is freely restartable and must reconstruct logic. It explicitly requires process-level survival but leaves active protocol-worker reconstruction dependent on broker stream replay.\"},{\"path\":\"docs/DEBUG-ROLLOUT.md\",\"description\":\"Operational update rules: normal signed apply, brain-only vs broker-touching classification, and refusal of broker-touching updates while broker-held runtime is live. Confirms default refresh/apply should not cycle broker resources.\"}],\"architecture\":\"Four distinct layers must not be conflated:\\n\\n1. PTY data plane / resource ownership. `spt daemon run` is the long-lived broker process. `Broker` owns the PTY master/ConPTY/forkpty session, child process, `OutputLog`, input journal, NetHost, live connection table, and per-stream `StreamLog`. None of those OS PTY handles are handed to the brain. This is why terminal children and rings remain alive during refresh.\\n\\n2. Operator/viewer connection. The `spt rc` process has its own local broker IPC `Brain` connection. It opens an operator-side network stream row and subscribes to output records. Its physical broker IPC connection and broker-held stream survive target brain refresh. It only reconnects on broker IPC EOF or network stream EOF; target pump death alone produces neither.\\n\\n3. Target-side controller/viewer bridge. The brain-owned inbound dispatcher enumerates peer-initiated stream rows. Each row gets a worker with a fresh broker IPC connection. `serve_attach` subscribes that peer stream, interprets the replayed Request, and subscribes the PTY OutputLog as Control/Viewer; a second connection forwards PTY output onto the operator stream. This worker is the actual controller bridge. Its physical broker connection is logged as `role=brain`, then gains `stream-subscriber` and controller/session attribution; `role=brain` is the protocol handshake class for all IPC clients, not proof that it is the singleton heartbeat brain.\\n\\n4. Daemon brain and broker lifecycle. Refresh/in-place apply sends `KIND_BRAIN_RESTART` over a transient CLI broker connection. The broker acks acceptance, its supervisor hard-kills the whole brain process, and respawns it. All dispatcher workers die with that process. Broker connection cleanup removes their logical subscriber/controller roles but not PTYs or streams. The new brain's `resume_session_cursors` restores only main-brain cursor knowledge without subscribing. Independently, first-heartbeat `spawn_net_consumers` starts a new dispatcher that is supposed to reconstruct active stream workers from broker truth.\\n\\nThe reconstruction boundary is not currently durable enough. Broker stream truth records direction, IDs, connection, origin, sequence/ring, and finished state, but not protocol family or attach opener metadata. The successor therefore depends on sequence-0 payload replay. Because that payload can be cap-evicted and claim failure is permanent for the generation, a resource-surviving refresh can leave an operator stream and PTY alive with no target bridge—precisely the detach/reattach-heals shape. The safe architectural direction is to make protocol/attach claim identity broker-durable (or otherwise recoverable without ancient payload replay) and make dispatcher claims transactional/retryable: in-flight only while a worker is starting, committed while a worker is serving, released/retried on recoverable failure. Readiness should also not imply controller continuity until restartable protocol workers have been reconciled, or the CLI must explicitly say it acknowledged rather than completed the refresh.\"}}}","call_id":"call_9IhPUC3Szah6xYn6LwINEP6A","name":"yield"}]},"duration":145618.6902999999,"ttft":4019.718799999915,"contextSnapshot":{"promptTokens":207559,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9IhPUC3Szah6xYn6LwINEP6A|fc_0698befac979932d016a58ac302af0819a9eab9b224b167b97","toolName":"yield","startedAt":"2026-07-16T10:04:28.668Z"},"id":"eac29532","parentId":"552a0421","timestamp":"2026-07-16T10:04:28.668Z"}
{"type":"message","id":"d9a701b9","parentId":"eac29532","timestamp":"2026-07-16T10:04:28.670Z","message":{"role":"toolResult","toolCallId":"call_9IhPUC3Szah6xYn6LwINEP6A|fc_0698befac979932d016a58ac302af0819a9eab9b224b167b97","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Diagnosis: the parent hypothesis is directionally correct about which process death interrupts controllers, but incomplete about intended restoration. `spt daemon refresh` and normal in-place `spt update` kill only the supervised brain process. That kill destroys every brain-owned inbound-dispatch worker and its target-side `serve_attach` pump, while the broker retains the PTY child, PTY master, NetHost connection, both stream rows, stream rings, and the operator-side `spt rc` broker connection. However, the successor brain is intended to restore active controllers separately from `run_brain`'s cursor-only PTY-session resume: after its first 500 ms heartbeat, `spawn_net_consumers` starts a fresh dispatcher, which enumerates broker-held peer-initiated streams and re-runs `serve_attach`. Therefore `BRAIN_RESUMED` only mentioning session cursors is not by itself proof that rc streams are abandoned.\n\nThe strongest code-proven causal candidate is the dispatcher's non-durable, one-shot reconstruction protocol. `dispatch::run_dispatch_loop` creates a fresh per-brain `claimed: HashSet<u64>` and inserts each stream ID before spawning its worker (`dispatch.rs:205-225`). A worker reconstructs the protocol family by subscribing from sequence 0 and replaying the first NDJSON record (`dispatch.rs:245-301`). Attach stream openers are not durable metadata: the original `AttachRecord::Request` is merely sequence 0 in an ordinary bounded-overwrite `StreamLog`. `DEFAULT_STREAM_RING_CHUNKS` is 4096 (`nethost.rs:108-111`); `StreamLog::append` evicts the oldest entries forever once the ring exceeds the cap (`nethost.rs:206-225`). Ordinary attach streams are not made retentive. After the opener rolls out, `peek_first_line` calls `Brain::net_stream_subscribe(stream_id, 0)`, which explicitly seeds cursor 0 (`brain.rs:1527-1542`); the first retained frame has sequence >0, so `Brain::read_event` rejects it as `stream … gap: got seq … want 0` (`brain.rs:807-830`). The worker returns `DispatchOutcome::Failed`; the dispatcher logs `DISPATCH:<id>:Failed(…)` but never removes the ID from `claimed`, so it will never retry during that brain generation. Detach plus `spt rc` opens a fresh stream with a retained Request and a new, unclaimed stream ID, exactly matching the observed repair.\n\n[INFERENCE] The 4-of-5 partial observation is naturally explained if four older/busier controller request-side streams had advanced beyond their retained opener while one newer/quieter stream still retained sequence 0. The cap is measured in transport read chunks, not logical input records, and QUIC/loopback may coalesce or split writes, so the exact age/input threshold varies per stream. A second compatible explanation is independent worker failure: because each stream gets its own IPC connections but every claim is one-shot, transient replay/connect/subscribe failure can strand any subset while another succeeds. Logs can discriminate: look for `DISPATCH:<stream>:Failed(\"stream <stream> gap: got seq … want 0\")`; other `DISPATCH:*:Failed`, `CONN_WRITE_RETIRED`, or `CONN_WRITE_POISONED` records would support the transient-worker arm.\n\nRanked falsifiable candidates:\n1. HIGH — opener evicted from the bounded broker stream ring, producing a replay gap on successor-dispatcher classification, followed by permanent one-shot claim. This is fully established as a reachable source-level failure; field causality remains [INFERENCE] until the generation logs show the gap or the deterministic rig reproduces it.\n2. MEDIUM-HIGH — any transient successor worker failure is made permanent by inserting into `claimed` before work and never clearing/retrying Failed/Unknown. Falsify by showing all affected stream workers reached `serve_attach` and emitted successful stream/controller subscription lifecycle events after the new generation.\n3. MEDIUM — the refresh acknowledgment/readiness ordering creates a real pause and increases replay races: the CLI returns after the broker acknowledges only that the restart signal was accepted; the supervisor consumes it on a 200 ms tick, kills the old child, and the successor writes `brain.ready` before net consumers are spawned. Dispatcher startup waits for the first 500 ms heartbeat/net-status. This explains a short interruption, not a persistent until-detach stall by itself.\n4. LOW — bounded broker-connection write retirement during kill/replay can independently fail some workers. Current `BrokerConn` bounds and poisons physical IPC writes, so this should self-clean rather than globally wedge, but because dispatcher claims are one-shot even an ordinary retired worker connection can become persistent at the stream level.\n5. LOW / no direct evidence — inherited client pipe handles could delay old brain-connection EOF cleanup if a descendant inherited one. The source contains no transfer of ConPTY/forkpty handles into the brain: those remain broker-owned. `spawn_brain_child` is a normal managed child with `CREATE_NO_WINDOW` on Windows, not a detached broker/PTY owner. The designated long-lived detached daemon/shell paths use `bInheritHandles=FALSE`; ordinary short role spawns are not the resource boundary here. Delayed EOF also does not block a new subscriber: both stream and controller cleanup use pointer-identity guards, so late old-connection cleanup should not detach the newer subscriber. This candidate does not explain 4/5 as cleanly as the per-stream claim/replay fault.\n6. REFUTED for the reported refresh/default-update shape — service manager or broker lifecycle. `spt daemon refresh` never calls service start/stop. Normal bare `spt update` and `update fetch --apply` use brain-only `apply_staged`; the service manager is involved only in `update apply --finish` / `spt update --restart`, which deliberately stops and restarts the whole daemon. That full-cycle path kills broker PTYs and re-runs endpoints, unlike the reported live-PTY/stalled-view symptom.\n\nExisting coverage proves adjacent invariants but misses this contract. `daemon_refresh_e2e.rs` proves broker PID, harness PID, and ONLINE perch survival, but has no active `spt rc` stream/controller. `brain_survive.rs` proves PTY + QUIC resources survive a process swap, but not that a long-lived production-dispatched attach continues. `attach_survives_target_brain_restart_exactly_once` manually constructs life 2 and calls `serve_attach` from 0 while the opener is still retained; it bypasses production dispatcher claim/classification and ring-floor aging. Dispatcher tests cover normal production serving and surviving an unrelated Unknown stream, not restarting the dispatcher over an existing stream or retrying a failed claim. `resume_no_control_steal_e2e` and `brain_resume_conn_deadlock` test direct PTY subscribers/session resume, not the broker-held network stream and target-side attach worker. `rc` reconnect tests exercise broker/transport EOF; a brain-only restart leaves the broker and operator stream alive, so the frontend receives neither broker EOF nor stream EOF when the target serve worker vanishes and therefore does not enter reconnect.\n\nProposed safe deterministic feedback loop (not executed because this assignment is read-only): add a test-only NetHost constructor/ring-cap seam so the attach stream ring can be 2–4 chunks rather than relying on 4096 timing-sensitive OS reads. Start the production dispatcher and a real echo PTY under an isolated broker; establish an rc-equivalent loopback Control attach; prove output/input first. Advance the peer/request-side stream enough to evict the Request, then terminate dispatcher generation 1 and start generation 2 while keeping broker, PTY PID, operator connection, and stream IDs fixed. Require the SAME operator stream—without detach, redial, or a new stream ID—to accept new input and render its echo within a short deadline. Current code should fail with the stream-gap/one-shot-claim signature. Add two companion arms: (a) opener retained, which must already pass, preventing an overbroad restart fix; (b) injected fail-once worker/connect outcome, which must be retried and then pass, proving claim lifecycle rather than only opener persistence. The end-to-end production gate should then extend `daemon_refresh_e2e`: active controller before real CLI `daemon refresh`; brain PID/generation changes; broker and PTY PID stay fixed; same controller stream receives post-refresh echo; no detach/reattach. Capture brain/broker logs and assert a new-generation stream-subscribe plus controller silent re-take, not merely `brain.ready` or perch ONLINE.","files":[{"path":"crates/spt/src/cli.rs","description":"CLI dispatch and update shapes. `DaemonCmd::Refresh` routes at 1471-1477 to `cmd_daemon_refresh` at 4064-4090. Update dispatch is 1443-1456. `cmd_update_apply` at 4890-4955 chooses live `apply_staged` vs daemonless apply. `plan_update_legs`/`cmd_update_composite` at 4958-5044 make normal bare update FetchApply and reserve whole-daemon Finish for `--restart`. `cmd_update_apply_finish` at 5140-5229 explicitly stop/wait/start cycles the broker. `cmd_update_fetch` at 6110-6333 routes `--apply` to `cmd_update_apply(false)`."},{"path":"crates/spt-daemon/src/applyhost.rs","description":"Shared refresh/update handoff. `apply_staged` at 299-348 connects before swap, swaps/records pending, then calls `Brain::request_brain_restart`. `refresh_brain` at 354-368 performs the same restart request without swap/staging. The returned boolean is only the broker's honored acknowledgment, not restart completion."},{"path":"crates/spt-daemon/src/brain.rs","description":"`Brain::request_brain_restart` at 1459-1467 sends `KIND_BRAIN_RESTART` and waits only for `BrainRestarted{honored}`. `resume_sessions` vs `resume_session_cursors` at 1028-1090 distinguishes subscribing/draining callers from the supervised non-draining brain. `net_stream_subscribe` at 1527-1542 seeds the requested cursor. Stream data at 807-830 rejects a first retained sequence above that cursor as a gap."},{"path":"crates/spt-daemon/src/broker.rs","description":"Broker owns PTY sessions and client-role cleanup. `handle_conn` at 2970-3220 labels every IPC client generically `role=brain`, serves commands, and on connection end detaches only that connection's PTY/stream roles while leaving sessions/rings alive. Thus `role=brain stream-subscriber` logs can be per-attach dispatcher workers, not the main supervised heartbeat brain. `dispatch_brain_restart` at 3877-3898 raises the supervisor atomic and acks. `dispatch_subscribe` at 3449-3482 resolves a target serve worker to controller/viewer. `OutputLog::resolve_subscribe` at 1147-1210 silently re-takes controller for the same remote identity, enabling intended restart re-serve. `any_local_controller_wedged` at 2910-2940 is promotion gating only, not active rc stream reconstruction."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Exact process transition. `BrainRestart` at 106-140 is an atomic coalescing signal. `supervise_brain` consumes it at 887-891, hard-kills/waits the current brain, increments generation, and respawns with `StartReason::Update` at 913-922. `run_brain` at 155-315 cursor-resumes PTY session metadata, writes ready, starts brain-owned hosts, then on the first 500 ms heartbeat starts net consumers. `spawn_net_consumers` at 321-335 starts the dispatcher. `spawn_brain_child` at 1018-1069 is a managed plain child (`CREATE_NO_WINDOW` on Windows; own process group on Unix), not owner of broker PTY handles. `spawn_brain_supervisor` at 1095-1131 wires the production broker truth/reaper."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"Primary defect locus. `run_dispatch_loop` at 198-232 creates an in-memory per-generation `claimed` set, inserts before spawning, and never consumes worker completion to retry/remove Failed/Unknown. `peek_first_line` at 245-279 always subscribes from 0. `worker` at 283-302 classifies that replayed opener and returns permanent Failed/Unknown on replay/connect errors. Attach arm at 333-394 resolves endpoint/session and calls `serve_attach`. `spawn_dispatcher` at 690-697 logs only loop-level termination; per-stream failures are `DISPATCH:<id>:…`."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-held network data plane and opener-loss mechanism. `DEFAULT_STREAM_RING_CHUNKS=4096` at 108-111. `StreamLog` at 150-291 stores the opener only as ordinary ring data; `append` cap-evicts oldest frames at 206-225; `attach` replays only retained frames >= requested cursor at 256-281. `register_stream` at 640-700 keeps read pumps and rings broker-side across brain death. `open_stream` at 1260-1370 creates paired loopback stream rows for local rc. `stream_infos` at 1420-1450 is what successor dispatch enumerates. `subscribe_stream`/`detach_stream_sub` at 1500-1533 use subscriber replacement and pointer-guarded late cleanup."},{"path":"crates/spt-daemon/src/attach.rs","description":"Target-side control pump owned by dispatcher worker inside the brain. `serve_attach` at 235-540 subscribes the peer stream, creates a separate forwarding broker connection, replays `AttachRecord::Request`, and at 333-416 attaches to the PTY as remote Control/Viewer. When the brain is killed this worker dies without finishing the broker-owned network stream. A successful successor worker can silently re-take the same identity and continue; without it, PTY and operator stream remain alive but no process forwards between them."},{"path":"crates/spt/src/rc.rs","description":"Operator-side topology and symptom. Module docs at 1-24 establish rc as a broker-net-stream pump; `establish_attach` around 1330-1510 opens/subscribes the operator stream. `attach_viewport` at 1519-1628 reconnects only after `BrokerGone` or `Severed`. `pump` at 1820-2000 retries read timeouts forever after any prior event; it enters reconnect on broker IPC EOF or target stream EOF. Brain-only refresh preserves both broker and stream, while the dead target worker sends no EOF, so an established rc can remain silently blank until operator detach/new attach."},{"path":"crates/spt-daemon/src/conn.rs","description":"Physical broker IPC write boundary. `BrokerConn` provides bounded/cancelable/poison-on-failure writes, with lifecycle attribution. This prevents one dead consumer from globally pinning broker writes, but a per-stream worker connection retirement can still feed the dispatch one-shot-claim defect. Relevant log discriminators are `CONN_WRITE_POISONED`, `CONN_WRITE_RETIRED`, and `CONN_LIFECYCLE`."},{"path":"crates/spt-daemon/src/daemon.rs","description":"Broker lifecycle and handle boundary. `Daemon::run` binds/serves Broker/NetHost and starts the brain supervisor at 165-367; graceful whole-daemon stop raises brain_stop and reaps the brain subtree. `start_daemon`/`stop_daemon` at 554-596 invoke service routing only for explicit whole-daemon lifecycle. `spawn_detached` at 610-720 and Windows `detached_no_inherit` at 738-858 concern initial long-lived broker startup and captured-pipe safety, not routine brain refresh."},{"path":"crates/spt-daemon/src/service.rs","description":"Service-manager boundary. `plan_start`/`plan_stop` at 57-78; Linux registered default-home daemon uses `systemctl --user start/stop` at 149-180. Windows logon task is boot-only and `detected=false` at 245-265. Refresh and normal brain-only update do not traverse this module; finish/restart does."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Existing real CLI refresh gate. It verifies brain PID/generation cycle, unchanged broker PID, live harness PID, and ONLINE perch, but never opens an active rc/controller stream or checks same-stream post-refresh input/output. This is the central missing acceptance seam."},{"path":"crates/spt-daemon/tests/attach.rs","description":"`attach_survives_target_brain_restart_exactly_once` at 772-918 manually drops life 1 and explicitly calls `serve_attach` for life 2 from sequence 0. It validates retained-ring replay and journal dedup but bypasses production dispatcher classification/claim and does not age the Request beyond the ring floor."},{"path":"crates/spt-daemon/tests/dispatch.rs","description":"Production dispatcher tests cover normal families and `dispatcher_survives_an_unknown_stream` at 1092-1155, which proves one bad stream does not kill processing of a later stream. They do not require retry of the bad stream, restart a dispatcher over existing attach streams, or test opener eviction."},{"path":"crates/spt/tests/brain_survive.rs","description":"Process-level PTY + QUIC survival across swapped brain binaries. It proves the broker resource plane survives but not active controller forwarding through a successor production dispatcher."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Proves brain respawn does not seize direct PTY controller connections and promotion still succeeds. It does not exercise broker-held rc network streams or dispatcher reclassification."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Guards the former undrained session-subscription self-deadlock and justifies cursor-only main-brain resume. It is orthogonal to per-stream dispatcher workers, which intentionally subscribe and drain."},{"path":"docs/KNOWN-HAZARDS.md","description":"Domain invariants: §6.7 broker/brain process isolation, §7.36 subscriber/control decoupling, and §7.38 bounded physical broker writes. These support keeping PTY and network resource ownership broker-side while treating brain-owned view pumps as reconstructible control-plane state."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Architecture ruling: broker process owns PTY masters, children, sockets, NetHost/QUIC and cursor truth; brain is freely restartable and must reconstruct logic. It explicitly requires process-level survival but leaves active protocol-worker reconstruction dependent on broker stream replay."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Operational update rules: normal signed apply, brain-only vs broker-touching classification, and refusal of broker-touching updates while broker-held runtime is live. Confirms default refresh/apply should not cycle broker resources."}],"architecture":"Four distinct layers must not be conflated:\n\n1. PTY data plane / resource ownership. `spt daemon run` is the long-lived broker process. `Broker` owns the PTY master/ConPTY/forkpty session, child process, `OutputLog`, input journal, NetHost, live connection table, and per-stream `StreamLog`. None of those OS PTY handles are handed to the brain. This is why terminal children and rings remain alive during refresh.\n\n2. Operator/viewer connection. The `spt rc` process has its own local broker IPC `Brain` connection. It opens an operator-side network stream row and subscribes to output records. Its physical broker IPC connection and broker-held stream survive target brain refresh. It only reconnects on broker IPC EOF or network stream EOF; target pump death alone produces neither.\n\n3. Target-side controller/viewer bridge. The brain-owned inbound dispatcher enumerates peer-initiated stream rows. Each row gets a worker with a fresh broker IPC connection. `serve_attach` subscribes that peer stream, interprets the replayed Request, and subscribes the PTY OutputLog as Control/Viewer; a second connection forwards PTY output onto the operator stream. This worker is the actual controller bridge. Its physical broker connection is logged as `role=brain`, then gains `stream-subscriber` and controller/session attribution; `role=brain` is the protocol handshake class for all IPC clients, not proof that it is the singleton heartbeat brain.\n\n4. Daemon brain and broker lifecycle. Refresh/in-place apply sends `KIND_BRAIN_RESTART` over a transient CLI broker connection. The broker acks acceptance, its supervisor hard-kills the whole brain process, and respawns it. All dispatcher workers die with that process. Broker connection cleanup removes their logical subscriber/controller roles but not PTYs or streams. The new brain's `resume_session_cursors` restores only main-brain cursor knowledge without subscribing. Independently, first-heartbeat `spawn_net_consumers` starts a new dispatcher that is supposed to reconstruct active stream workers from broker truth.\n\nThe reconstruction boundary is not currently durable enough. Broker stream truth records direction, IDs, connection, origin, sequence/ring, and finished state, but not protocol family or attach opener metadata. The successor therefore depends on sequence-0 payload replay. Because that payload can be cap-evicted and claim failure is permanent for the generation, a resource-surviving refresh can leave an operator stream and PTY alive with no target bridge—precisely the detach/reattach-heals shape. The safe architectural direction is to make protocol/attach claim identity broker-durable (or otherwise recoverable without ancient payload replay) and make dispatcher claims transactional/retryable: in-flight only while a worker is starting, committed while a worker is serving, released/retried on recoverable failure. Readiness should also not imply controller continuity until restartable protocol workers have been reconciled, or the CLI must explicitly say it acknowledged rather than completed the refresh."},"status":"success","type":"result"},"isError":false,"timestamp":1784196268669}}
{"type":"message","id":"53832e40","parentId":"d9a701b9","timestamp":"2026-07-16T10:04:28.682Z","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":1784196268676}}
{"type":"message","id":"668f6be6","parentId":"53832e40","timestamp":"2026-07-16T10:04:28.686Z","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":"5f0ff3c6","parentId":"668f6be6","timestamp":"2026-07-16T10:04:44.390Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming final acknowledgment and completion**\n\n**Planning IRC acknowledgment before final yield**"},{"type":"toolCall","id":"call_OLMylNMrhULGXkwCzTe3l7zs|fc_012800a65e9528cd016a58acb9e984819ab65aa91fccbbb701","name":"irc","arguments":{"i":"Acknowledging RCA handoff","op":"send","to":"Main","message":"Acknowledged. My finalized findings are submitted; no further exploration. The evicted-opener/one-shot claim root is supported by the exact source chain and complements your stale finished-stream primary.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8745,"output":320,"cacheRead":204288,"cacheWrite":0,"totalTokens":213353,"reasoningTokens":226,"cost":{"input":0.04372500000000001,"output":0.009600000000000001,"cacheRead":0.102144,"cacheWrite":0,"total":0.15546900000000002}},"stopReason":"toolUse","timestamp":1784196268715,"responseId":"resp_012800a65e9528cd016a58acad3998819a93eafc7a39680706","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_012800a65e9528cd016a58acb67b68819aa147adbd66366703","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKy5zZ26lvm1QYpN_j3Nb-REhfU8-Pcf1CRjufaAdTvkM-9NhmlWbEmLXYuRk54cYoeswAMVEdPV_wLZYB3S6JikzpyrQCdQkY2zjwFH5UYHg6VPLd8QdmEsMv1iQiJNTzrOYim79Arh9RA2cp0DKJ1lCTLxCTuRIZ7TalwH33u0pLXRw-foL4Rw3vTthqX9gohhDTsH-J4MVCq_e7Rd4mH_ijZIDODhwT6ncgikifBLFKNhytqtADxU7Xdh54xPsQ-mlleCtDnJPzSJx6d42I7hqEkDOaVshVBxhT5wdYkedw3wbXN44WeqIQ91s68Do2qsOfFS5NzeBd5bGsc3AGcONdNyIZVkr8AO0eCNR1AXxtmLOpKU5QqX_00jDJI9m0Pg5dMaXLIvJIbYhpsI0WhEJmNyPTwp2VhEghsPm7zOZ2RflihXKsOhAtXMiFxq9H41cxo1FBoG5AKgIqGiirKAb84GPYmLfU5FvUcPRexsR9IszujayLzgwz66rYM3_Rts8P4uBLfvMfRrwNpkBXUAtWq0lIgCjT3rg2RxRz5hGu6CWdStuR2ZNpvJoyKlNIdbXjIZjC3emUvioUkiM_B6uP9AOToLfqVDYveylM2KktSI6KQN-s9NMasqeBP-JPWAXlETNxv-FhrRqRSH2mwNaxXgzcGUVhsuD32jz8bp5zWCuwvP6dd9ezKUrxLMltXSLViK5hOkIOEPeH_tChA6oE9TwJYzepOUyLt0R_OehRUlT-E8gMbswl50GDMyNeAqXLY3tMtjOv-j24F3Q0uyk55FsJhAXpjq6pYcFu951ScJeKxkccN0n3j0ODw9jhIx48nvhHIIopxs6HKfkHADvP2HtUSCzrVN35lQUpiCXhpHMrxwe2LlptxgWkHtOZ3OTZ1AAwwk2JhD4N1EcOYTHlQFTsxTJ56zOYaqiyoaWdEoK8UDFbNI4RGYN2IhJc9zT9C8svLL_YbJq2pVXgFetGCIQahdJrim9g8J_VfDDMRiXb1H27yLpaHI_xA6iTWfzuwGqi64DT-CsadS5sxbrnVU3_kBny9aaKrxKFLBKibn-QwWEOwvdSvCL_X04rbxMgmxx75xy6Yg9q-IkhvVTT8lDK3yD7uZ5unPqL40WeSFWq9WIvclEr6aEM71EVjezJHT0vBC63FlkWa8FMK1j0W4_rovie5ccrnW0BzdEvDUTdeAMXrboS5ErxKh5nGZkyxYSob043fluc0nqADlF7nA_fqGo9v0Jg8imjHWf_kDFNihImnS0mJBKV99JcMS6nZUML5kvZAEQ_CmrqA69gxroL0z66RA30RN0grXdOQwnxw2_IJXjCMsr5yu9pk6sm0RL2mKDUoFF11ioomwsLMbQmtUAMxd4-MEaHiulJ-rJQqG32YzIFGZvFj48Gkjms3cTZgR1ee1_0FHoi3XRAXNgUKAqxdcnSDi7W_SuiFm9UVvWImayxLx2-iGybKgc8D0wxQU6HWssWncX7rMd-8FFSeoA4ZXaucsfj29rhA4GsvaW-KYyp-Lcc5wVI7hK09oelAJkI9WiVhz5chtdzvQC1F5037VG9qdReMfNECJtwvVoX94l4odU6bJ9kjoUM77p-U99mhcNYEfoAre7VAptxGyffwc3xB-1nnb34EN7x0kbt0pAbBkMS5lHOjpbys_9KFlKsWcXBeoHJHsIPGtVdw1B_2qSUqi-eL0i4_AUoE78oESlVQDGjQ5EMEmYondGs9cZEyBfbs2_brfXEZq42k74LZ8txJ76ebL0NpeCM7SKGDo00wSe8IlfbmIy4FtQQL9k_IXkTxKO5iha6ypSL-0YqCb2cRT7nB0qKkMbdndIGe8BR3mF_2I2aC_4ISrqe2yhjB1UvqlYOC-fpNVPZu7rvCp8UghSWWNpfWM_t5Sp2MOYbJqxMPk3cLBgOGCtUzEGq1DTZD9mqIkyZJ8SsO54P7zpTQ2E_sNjm3nVaDpg0vmv9oiPn7-hpUn2u24ioSkPEjW27dKJVfsTpU2IlvvGrGh4eOenc9MeF56kGChgk0ipRuU_VsOwGxW6sS3Rz0AiW3mboLn4Sfu3KY4886tQ0qSSO0m-eCDwc1JEd3nwW3NDZ0cbV0el3rRvlMQM3ETFpQfLOiyjI549MdZQQqt9YskpJVlBgM5ik9ZqMr4lucCjcPBeyiebiYv55y_Judf9ckPklQpZ8PFNxLgx-Pm3ZY6xn_wDMumTsrDFYTOaYBhlx90sAyU1WHYGB7NrA_W_iIanrjVNp70LxpeNcn6ROZBD41CDRRxq6WfDw-hQsJwvjPEyKgDuyB_1yCTH1x72JceV2NHsfBYxebN4OLkO7_NKbeXaYqJBCtymJaYyY7T2El7KxsivukJ1LZnUbX3GRZEdauAHdR-C6pWYBe023rq0DRIZiyyZ9lO1d81TwPJCZ_lggFzf8mhyKG0wRUC1_x8cGuDojTyw3lXcQ7JQOpg9K1XXzHmS1vZlIYI1ukKXAdd-anmPMOjTSZhl97xTCSbJZ6_NywZYw==","summary":[{"type":"summary_text","text":"**Confirming final acknowledgment and completion**"},{"type":"summary_text","text":"**Planning IRC acknowledgment before final yield**"}]},{"id":"fc_012800a65e9528cd016a58acb9e984819ab65aa91fccbbb701","type":"function_call","status":"completed","arguments":"{\"i\":\"Acknowledging RCA handoff\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Acknowledged. My finalized findings are submitted; no further exploration. The evicted-opener/one-shot claim root is supported by the exact source chain and complements your stale finished-stream primary.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_OLMylNMrhULGXkwCzTe3l7zs","name":"irc"}]},"duration":15673.685599999502,"ttft":9747.603999999352,"contextSnapshot":{"promptTokens":213033,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OLMylNMrhULGXkwCzTe3l7zs|fc_012800a65e9528cd016a58acb9e984819ab65aa91fccbbb701","toolName":"irc","startedAt":"2026-07-16T10:04:44.390Z","intent":"Acknowledging RCA handoff"},"id":"bc446d2b","parentId":"5f0ff3c6","timestamp":"2026-07-16T10:04:44.390Z"}
{"type":"message","id":"b6f907b9","parentId":"bc446d2b","timestamp":"2026-07-16T10:04:44.391Z","message":{"role":"toolResult","toolCallId":"call_OLMylNMrhULGXkwCzTe3l7zs|fc_012800a65e9528cd016a58acb9e984819ab65aa91fccbbb701","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"RefreshLifecycle","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784196284390}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `ReproHarness`:\n\nFound 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.\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: \"ReproHarness\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"1531e5a2b91c5ea2","from":"ReproHarness","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."},"attribution":"agent","id":"1a96e32d","parentId":"b6f907b9","timestamp":"2026-07-16T10:04:44.391Z"}
{"type":"message","id":"79d4c9c5","parentId":"1a96e32d","timestamp":"2026-07-16T10:04:51.322Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_J69M6riyuPdhQeH2Gx4mc5c6|fc_012800a65e9528cd016a58acc143f4819aa3eb1b1a68813290","name":"irc","arguments":{"i":"Confirming shared root","op":"send","to":"ReproHarness","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.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3117,"output":127,"cacheRead":210432,"cacheWrite":0,"totalTokens":213676,"reasoningTokens":15,"cost":{"input":0.015585000000000002,"output":0.00381,"cacheRead":0.10521599999999999,"cacheWrite":0,"total":0.124611}},"stopReason":"toolUse","timestamp":1784196284402,"responseId":"resp_012800a65e9528cd016a58acbccd24819a989f7f9b550f4061","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_012800a65e9528cd016a58acc044d4819ab047f1fb390df374","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKzBn2QKWv_dRZ7gvQItuGCGbTeZjP4lLW78z2HXSv0wx9ocj1mY-Ib_n-f-CNT2bINZlioI4dUcV-9B-mLFJOPFDJ5fVn76jL_NNus6bW1L_Pw2e--z1xf1a-HGs8-pvvgaMo1RKH7tPzmZ4oR6qRaD0vURfiWtkwHWU-aIyL_qZ-r-ZJjlzNX5k3ZRW5LDq7nBa2NGHN2pI0L7jpTfrcYegYh-IYxHZlIVuP8jxUnYS_R-l8AF_UnO5FH_AGuW_2n4JXIwOkvXLDRmEItmqdIS82i6VUMFUy93NNscxTlPY6dejMe98ni5KMFktjlRWXud-6Yf0yQkeP9zfk5gXqH2wJrT6HysJ2y9SOPesFQD6Wm6NH1ebXbu11mFQ7xBq85Sl3cUvquR3TbBqdRuv1ulhFfdDo5tGs3v9I6XRff3w_z149hNuR_NvA0kipCQu547jE59P8ttt1EvnaLxKb703oVbdsoi16OTg67R97k5wfJiJ1T5X_8LrjcCk7CmYmuSMFpJD9WWgeCQxOlA5sLER-i-F9y-wYalGpcp-Q_lf101bKjbFXwDjjrraE1uLzGEkwRLSK3PFuXONzoYePrg8jSXQv5bi_EyO_88vER40pCRh98qfJEeMpZMp8dlsYqcI_Jn13Kh06g430_BoHTt7cIjfj9IgGCHFRyfODuUCsplXX76IorNiDrfjpXHAtnfQunnpRlANQBSS__3u1XYVnRMwGWVJD_FWxGxv3RNCjgPHc4GwfBGlfVYZEY3H4uV1OHR88YKu22Dk_jXQggvQX8NOUrcdlLn9tbui3sJtVAHpH8P6drthGeTaj8A2gHkxo-s7umgMJoIpmsxE14NG7C8mcGHWmbNik56RV7xlzfvdtiq9Np1yXkQAoYPlz-70D97sr9MZPO0fqa1cFKmYCYcXJBcgd9csc8HphadpWABPmYyEs2OiChHVgGgODcbhrdSev-5fS_E0WrLmOQ0fahnv_oyrypOVPJ120J2Gac=","summary":[]},{"id":"fc_012800a65e9528cd016a58acc143f4819aa3eb1b1a68813290","type":"function_call","status":"completed","arguments":"{\"i\":\"Confirming shared root\",\"op\":\"send\",\"to\":\"ReproHarness\",\"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.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_J69M6riyuPdhQeH2Gx4mc5c6","name":"irc"}]},"duration":6901.392600000836,"ttft":3869.2637000000104,"contextSnapshot":{"promptTokens":213549,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_J69M6riyuPdhQeH2Gx4mc5c6|fc_012800a65e9528cd016a58acc143f4819aa3eb1b1a68813290","toolName":"irc","startedAt":"2026-07-16T10:04:51.322Z","intent":"Confirming shared root"},"id":"f76db505","parentId":"79d4c9c5","timestamp":"2026-07-16T10:04:51.322Z"}
{"type":"message","id":"2ba90c8b","parentId":"f76db505","timestamp":"2026-07-16T10:04:51.323Z","message":{"role":"toolResult","toolCallId":"call_J69M6riyuPdhQeH2Gx4mc5c6|fc_012800a65e9528cd016a58acc143f4819aa3eb1b1a68813290","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- ReproHarness: injected"}],"details":{"op":"send","from":"RefreshLifecycle","to":"ReproHarness","receipts":[{"to":"ReproHarness","outcome":"injected"}]},"isError":false,"timestamp":1784196291322}}
{"type":"message","id":"7e79f239","parentId":"2ba90c8b","timestamp":"2026-07-16T10:05:14.598Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Finalizing concise summary output**"},{"type":"toolCall","id":"call_2SodDjA84VZXXAxmhX9pOY7l|fc_012800a65e9528cd016a58acc8a364819a978fce13e8a80d41","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Finalized and handed off. The lifecycle trace confirms that brain refresh/update kills brain-owned dispatcher/`serve_attach` controller bridges while broker-owned PTYs and network streams survive. Successor reconstruction exists, so cursor-only `BRAIN_RESUMED` is not the whole story; the adjacent code-proven failure is successor dispatch replay from sequence 0 after the bounded stream ring has evicted the attach opener, followed by a one-shot `claimed` entry that is never retried. This directly explains why detach plus a fresh `spt rc` stream repairs the viewport. The main agent has integrated this alongside the separately identified stale-finished-stream takeover primary RCA.","files":[{"path":"crates/spt-daemon/src/dispatch.rs","description":"Successor stream rediscovery, sequence-0 first-record classification, and pre-worker permanent `claimed` insertion."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-owned bounded StreamLog; 4096-chunk rollover can evict the attach Request while the stream and PTY remain alive."},{"path":"crates/spt-daemon/src/brain.rs","description":"Cursor-0 stream subscription and forward-gap rejection when replay begins above the evicted opener."},{"path":"crates/spt-daemon/src/attach.rs","description":"Brain-owned target-side bridge whose worker dies on refresh and must be reconstructed from the surviving broker stream."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Planned brain kill/respawn call chain and delayed successor dispatcher startup."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Existing refresh E2E proves process/perch survival but omits same-stream active controller continuity."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Existing restart test manually re-serves while the opener remains retained, bypassing production rediscovery and rollover."}],"architecture":"Broker owns the PTY data plane and network stream/ring resources; the operator `spt rc` owns a surviving frontend broker connection; the target `serve_attach` worker is brain-owned and dies during refresh; the successor dispatcher must reconstruct that bridge from broker truth. Current broker truth does not durably encode the attach protocol/opener, so reconstruction depends on bounded payload history and a non-retryable per-generation claim. The deterministic missing gate is a real refresh with an already-active controller whose opener has rolled below the retained stream floor, asserting that the same stream—not a detached/new stream—continues input/output while broker and PTY PIDs remain fixed."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2244,"output":605,"cacheRead":211456,"cacheWrite":0,"totalTokens":214305,"reasoningTokens":47,"cost":{"input":0.01122,"output":0.01815,"cacheRead":0.10572799999999999,"cacheWrite":0,"total":0.135098}},"stopReason":"toolUse","timestamp":1784196291335,"responseId":"resp_012800a65e9528cd016a58acc3a098819a9666caf0daa70a3d","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_012800a65e9528cd016a58acc64b08819a982dfaec553f87de","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKzI3nJFo7iDX5VCeu9z-gH7XSGDvbeXNDhtPwN6X-jjTOrq7IFYzzMeukxT4Fun_rFqhq6b4w3oNCCAPm-JoxLmbSb1sVMhajC26FseIPSVxumHdAPBZJi2SQj0Q9KXKsNvrMdBcIfYE1Xg7_Bm1dDleEn0PARVt3SgK9yvq7zWJONyx81A_INMmvKmQ9lxzheFee4aUTQv8sKJCFDntvlrNwlTV80txGMEb_JHGRkI3Iz5tFQoF-qtWAUU7WF2fo2mrmVgdsgEkqI6gjSAANTj7qwJaLgzz1zeKMlg1Y0u-2q_7EiOS-BcbRnxuUc2QmAhrirQhEU6ABcZ1bQbIpr8q8j_8aWS9JQBVM1DFlujuaeGsCD2IklTUk6y3IMQnddvflc3X_fXUgNFxZvxUlnH2jra5eTwVJeyylqZ0idj7coOGD1sGwzcSwATHtTnECM0YDfwQX4aHyOeTt8Ncw9g3rmfGUuGv9aBdnNsKgga19_5BHcBmSByb8td3Z-g0eNo7NdbniUmKWk99ISxjqhhs_9bRk9Ygztv_Ju1rdRBEmbWgqgVC5Vzg8YN_mcJEnLVxi5-DheCIVXFJbxBeTEYEzmtbwzT2sWmijREH2e_-46Ii0MUebz_9bNZA_NmZ_odjtiZFctEXPFvJsxcSfN_21tXui6aOO3ln-q0I8KcjNXSIwcEUW4VhYLifhDbWkuAJY74PxcJjvqNL0FR1JNW-dc2v7xk01CIZqFi7kzOafI9v1v4OtUpI6XwQml039ipoPfIjmg5wjZXg8Kf1glQZiQ9Dt1ntpZtvRzMt6CnP7McurGTNwDeDswXOgit1yxo9BuiDumVamIlTDhXft-hJ8HT9sSKM3SzAm8_d-nT1WuwNS0mVtUKArKSmjtlFRpaRlPJVixXHK4QRvxXWYtcPBXU67FuuZVbI-N-oZpWl2GBeTjxHHczuEa9O8WpVcmFoGP4ii9pOKbPlTcVtVCwL8Ee4iIBIZZw0yESLENuCo5WWZTnQoftUpH1gecSl_4f9lrpVZSq6F55-5yZ5G_pCOUBKj-yywXLuWoFK8jqOiKmU7_mG0Qs_zuGdx_SzVXEYV_hTO1oksRjADNyNVmO6bB37IyfALvZoORumqctFDd4JxUUCXLD-Rg1ldqN5aps6C76cpbdRQzWpNPpaSS-WkaE-fMqIeZCXcKycQ0QhXY0DdOk-27GnAQXUvevI2W_1me7IKaAg1jjOKCj7lBFPjAyegk0ORKF0TLb3VY4RWI=","summary":[{"type":"summary_text","text":"**Finalizing concise summary output**"}]},{"id":"fc_012800a65e9528cd016a58acc8a364819a978fce13e8a80d41","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Finalized and handed off. The lifecycle trace confirms that brain refresh/update kills brain-owned dispatcher/`serve_attach` controller bridges while broker-owned PTYs and network streams survive. Successor reconstruction exists, so cursor-only `BRAIN_RESUMED` is not the whole story; the adjacent code-proven failure is successor dispatch replay from sequence 0 after the bounded stream ring has evicted the attach opener, followed by a one-shot `claimed` entry that is never retried. This directly explains why detach plus a fresh `spt rc` stream repairs the viewport. The main agent has integrated this alongside the separately identified stale-finished-stream takeover primary RCA.\",\"files\":[{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"Successor stream rediscovery, sequence-0 first-record classification, and pre-worker permanent `claimed` insertion.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Broker-owned bounded StreamLog; 4096-chunk rollover can evict the attach Request while the stream and PTY remain alive.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"Cursor-0 stream subscription and forward-gap rejection when replay begins above the evicted opener.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"Brain-owned target-side bridge whose worker dies on refresh and must be reconstructed from the surviving broker stream.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"Planned brain kill/respawn call chain and delayed successor dispatcher startup.\"},{\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"description\":\"Existing refresh E2E proves process/perch survival but omits same-stream active controller continuity.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"Existing restart test manually re-serves while the opener remains retained, bypassing production rediscovery and rollover.\"}],\"architecture\":\"Broker owns the PTY data plane and network stream/ring resources; the operator `spt rc` owns a surviving frontend broker connection; the target `serve_attach` worker is brain-owned and dies during refresh; the successor dispatcher must reconstruct that bridge from broker truth. Current broker truth does not durably encode the attach protocol/opener, so reconstruction depends on bounded payload history and a non-retryable per-generation claim. The deterministic missing gate is a real refresh with an already-active controller whose opener has rolled below the retained stream floor, asserting that the same stream—not a detached/new stream—continues input/output while broker and PTY PIDs remain fixed.\"}}}","call_id":"call_2SodDjA84VZXXAxmhX9pOY7l","name":"yield"}]},"duration":23262.000799999572,"ttft":2955.8273000000045,"contextSnapshot":{"promptTokens":213700,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_2SodDjA84VZXXAxmhX9pOY7l|fc_012800a65e9528cd016a58acc8a364819a978fce13e8a80d41","toolName":"yield","startedAt":"2026-07-16T10:05:14.598Z"},"id":"3c85329a","parentId":"7e79f239","timestamp":"2026-07-16T10:05:14.598Z"}
{"type":"message","id":"5e8eb7f0","parentId":"3c85329a","timestamp":"2026-07-16T10:05:14.599Z","message":{"role":"toolResult","toolCallId":"call_2SodDjA84VZXXAxmhX9pOY7l|fc_012800a65e9528cd016a58acc8a364819a978fce13e8a80d41","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Finalized and handed off. The lifecycle trace confirms that brain refresh/update kills brain-owned dispatcher/`serve_attach` controller bridges while broker-owned PTYs and network streams survive. Successor reconstruction exists, so cursor-only `BRAIN_RESUMED` is not the whole story; the adjacent code-proven failure is successor dispatch replay from sequence 0 after the bounded stream ring has evicted the attach opener, followed by a one-shot `claimed` entry that is never retried. This directly explains why detach plus a fresh `spt rc` stream repairs the viewport. The main agent has integrated this alongside the separately identified stale-finished-stream takeover primary RCA.","files":[{"path":"crates/spt-daemon/src/dispatch.rs","description":"Successor stream rediscovery, sequence-0 first-record classification, and pre-worker permanent `claimed` insertion."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-owned bounded StreamLog; 4096-chunk rollover can evict the attach Request while the stream and PTY remain alive."},{"path":"crates/spt-daemon/src/brain.rs","description":"Cursor-0 stream subscription and forward-gap rejection when replay begins above the evicted opener."},{"path":"crates/spt-daemon/src/attach.rs","description":"Brain-owned target-side bridge whose worker dies on refresh and must be reconstructed from the surviving broker stream."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Planned brain kill/respawn call chain and delayed successor dispatcher startup."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Existing refresh E2E proves process/perch survival but omits same-stream active controller continuity."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Existing restart test manually re-serves while the opener remains retained, bypassing production rediscovery and rollover."}],"architecture":"Broker owns the PTY data plane and network stream/ring resources; the operator `spt rc` owns a surviving frontend broker connection; the target `serve_attach` worker is brain-owned and dies during refresh; the successor dispatcher must reconstruct that bridge from broker truth. Current broker truth does not durably encode the attach protocol/opener, so reconstruction depends on bounded payload history and a non-retryable per-generation claim. The deterministic missing gate is a real refresh with an already-active controller whose opener has rolled below the retained stream floor, asserting that the same stream—not a detached/new stream—continues input/output while broker and PTY PIDs remain fixed."},"status":"success","type":"result"},"isError":false,"timestamp":1784196314598}}
{"type":"message","id":"12386c1e","parentId":"5e8eb7f0","timestamp":"2026-07-16T10:05:24.253Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_012800a65e9528cd016a58acdd6168819a851aa91bc3f75e12\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2863,"output":4,"cacheRead":211456,"cacheWrite":0,"totalTokens":214323,"cost":{"input":0.014315000000000001,"output":0.00012,"cacheRead":0.10572799999999999,"cacheWrite":0,"total":0.12016299999999999}},"stopReason":"stop","timestamp":1784196314612,"responseId":"resp_012800a65e9528cd016a58acdb04dc819ab1981c3403a1c02a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_012800a65e9528cd016a58acdd6168819a851aa91bc3f75e12","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":9640.139299999923,"ttft":3057.4626000002027,"contextSnapshot":{"promptTokens":214319,"nonMessageTokens":15087}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-16T10:12:24.261Z"},"id":"cc344cc3","parentId":"12386c1e","timestamp":"2026-07-16T10:12:24.261Z"}
